Taking what I've learned, and applying it to MicroMacro

For only tutorials. If you would like to post a tutorial here, first post it elsewhere (scripts, general, etc.) and private message me (Administrator) a link to the thread. I'll move it for you.
Post Reply
Message
Author
User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Taking what I've learned, and applying it to MicroMacro

#1 Post by Administrator » Thu Jun 04, 2015 9:27 pm

So, now that I've got a job working on a pretty serious programming project, I'm getting more experience with proper programming techniques. I'm quite fond of some of the things I'm learning, and over time I've been copying what I like into MicroMacro. For example, the Timestamp library class was (loosely) based on PHP's Carbon timestamp class, mixed with the event chaining in Eloquent.

Today, I decided to apply what I've learned to helping with event handling, since that seems to be quite an annoyance for some. The methodology here is to create an event handler object, bind that to the dispatcher, and let everything else happen automatically. OK, that sounds more complicated than it needs to be, so lets just get to an example.

First, lets create our event handler object:

Code: Select all

TestHandler = class.new(EventHandler);

function TestHandler:constructor(vk, toggle)
	self.vk			=	vk;
	self.toggle		=	toggle;
end

function TestHandler:handle()
	printf("Event fired. Key: %s, Toggle: %s\n", self.vk, self.toggle);
end
You'll see we inherit from the base class EventHandler, and copy our event datas via the constructor. The handle() function is what gets called when the event is triggered. Pretty straight forward. So, onto main.lua, where we set it all up:

Code: Select all

require('event/dispatcher');

include("testhandler.lua");
function macro.init(script, ...)
	EventDispatcher:bindHandler('keypressed', TestHandler);
end

function macro.main(dt)
	return true;
end

function macro.event(e, ...)
	EventDispatcher:handle(e, ...);
end
You'll see that in macro.init(), we bind our event (keypressed) to our TestHandler object. Of course we could bind multiple events (but only one handler to each event), but just 'keypressed' is good enough for a test. In macro.event(), we pass the event data on to the dispatcher so that it can create the event handler for us and trigger the event. As such, whenever we press a key, TestHandler:handle() is called, and self.vk contains the virtual key we pressed, and self.toggle contains the key's toggle state.


So, why pass the arguments to the constructor instead of directly to handle()? That's a good question. I'm not entirely sure I have a great answer for you. However, it would allow for more abstraction, as we can create another child of our TestHandler with altered attributes that would then retain all the functionality of TestHandler without having to repeat code. It also means that we can pass around the object containing all the variables attached to it without any extra work. Finally, it separates the code better such that the constructor() takes care of all the setup (maybe there's something special we want to do with the arguments, before actually handling it?) and handle() just does the processing.


Anyways, while it normally isn't "hard" to handle the events, I think something along these lines could make things even easier and much more clean. I'm mostly just experimenting and seeing what and what doesn't. I just thought I'd share. Maybe it might inspire you guys on your own things, or maybe you've got some valuable input.
Attachments
event.zip
Extract to micromacro/lib
(1.25 KiB) Downloaded 273 times

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#2 Post by Administrator » Fri Jun 05, 2015 3:24 am

Part two of this is bringing in a real-world example to display why this is even anything special. At first glance, it looks like an overly fancy way to just call a function when an event hits macro.event(). While that's true, this kind of abstraction opens things up to a number of possibilities.

Lets pretend we have a bot script that will trap error messages. Whenever an error happens, it copies it into the character's log file, along with all the other information that was relevant to the character. Lets start with just that, using the event dispatcher.

main.lua:

Code: Select all

require('event/dispatcher');

include("errorhandler.lua");
characterName = "Bob";
function macro.init(script, ...)
	-- The scripts default event handlers would go here.
	EventDispatcher:bindHandler('error', ErrorHandler);
end

function macro.main(dt)
	error(sprintf("Some pretend error happened with the character named %s, so log it.", characterName), 1);
	return true;
end

function macro.event(e, ...)
	EventDispatcher:handle(e, ...);
end
eventhandler.lua:

Code: Select all

ErrorHandler = class.new(EventHandler);

function ErrorHandler:constructor(message)
	self.message		=	message;
end

function ErrorHandler:handle()
	local file = io.open(string.lower(characterName) .. ".log", "a");
	assert(file);
	file:write(self.message);
	file:write("\n");
	io.close();
end
Pretty straight forward. When the example is run, it generates a fake error. The error is raised, passed as an event, and dispatched by the event dispatcher, which then fires the event and saves the error message into the log file for the correct character. We should see this in bob.log:
main.lua:12: Some pretend error happened with the character named Bob, so log it.
stack traceback:
[C]: in function 'error'
main.lua:12: in function <main.lua:11>

Now, lets pretend our script also supports user-provided addons. It will automatically load any addons it finds, and we want those addons to be self-sufficient instead of requiring the user to modify the base scripts. Now, lets say that one of our addons decides that the base error logging isn't good enough. Not only that, but it wants to extend the error logging without modifying it (such that any improvements made to the base will be carried over when it is updated) nor replacing it.

So, how can our addon do all of that? Simple. It will extend the base error handler by creating a child of it and tacking functionality on. So, here we add in myerrorhandler.lua:

Code: Select all

require('timestamp');
MyErrorHandler = class.new(ErrorHandler);

function MyErrorHandler:handle()
	MyErrorHandler.parent.handle(self);

	local file = io.open(string.lower(characterName) .. ".log", "a");
	assert(file);
	file:write(sprintf("The time of the error was %s\n", Timestamp:now():toHuman()));
	io.close();
end

EventDispatcher:bindHandler('error', MyErrorHandler);
Now when this file is loaded, it directs error events to MyErrorHandler, which then calls the original ErrorHandler event code. When we run the script now, we see:
The time of the error was 2015-06-05 03:05:25
main.lua:12: Some pretend error happened with the character named Bob, so log it.
stack traceback:
[C]: in function 'error'
main.lua:12: in function <main.lua:11>
To be perfectly honest, I have no idea why the timestamp is being printed first over the error, but that is irrelevant for this example, so we're going to ignore it. The point is that it works. We can infinitely extend and add functionality on without outside modifications, without causing more errors than we started with, and without causing the code to become a complex, unreadable mess.

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#3 Post by Administrator » Fri Jun 05, 2015 1:50 pm

Part three now, going another step deeper.

Of course, we'll want to stuff our own events into this system instead of just the basic MicroMacro-provided events. We will also want them to retain functionality and code separation from others, just like in the above example. So, first, create a custom event handler:

Code: Select all

CustomEventHandler = class.new(EventHandler);

function CustomEventHandler:constructor(event)
	self.event	=	event;
end

function CustomEventHandler:handle()
	-- If the event has a fire() method, call it.
	if( type(self.event.fire) == "function" ) then
		self.event:fire();
	end
end
It's another bare-bones EventHandler object that simple accepts an Event object and calls its fire() method in order to handle it. We could create special event handlers for each and every event we want to raise, bind them to whichever token we want, and call them that way. However, using the event object means we only need 1 handler and we just let the event handle itself, saving some work. Just toss it all into 'custom' and be done with it.


Lets pretend we want to have an event triggered when the character has died, so we'll need to create an Event object for that:

Code: Select all

CharacterDiedEvent = class.new();
function CharacterDiedEvent:constructor(characterName, reason)
	self.characterName = characterName;
	self.reason = reason;
end

function CharacterDiedEvent:fire()
	local msg = sprintf("%s has died. Reason: %s\n", self.characterName, self.reason);
	print(msg);

	macro.event('error', msg);
end;
Again, we follow the same pattern as we did with the handlers: parameters to the event should be passed as the constructor. Why? Because when the event is triggered is where we gather the information, but we might not want to fire it right away, or we might want to pass it around to other related handlers, or we simply don't want to be clogging up with main loop runtime with these sorts of events.

You'll see that when the character has died, it displays the message on the screen, and it also throws its own event and pretends that it is an error so that another handler (our ErrorHandler) can also capture this event and make use of it.

But lets go a step further than that. Lets add yet another handler that captures hotkeys; when the user presses spacebar, that's when it pretends the character is dead and should fire the character died event, which in turn fires the error event, which should be handled by both MyErrorHandler and plain ErrorHandler:

Code: Select all

KeyPressedHandler = class.new(EventHandler);

function KeyPressedHandler:constructor(vk, toggle)
	self.vk = vk;
	self.toggle = toggle;
end

function KeyPressedHandler:handle()
	if( self.vk == key.VK_SPACE ) then
		-- Pretend the character had died.
		local event = CharacterDiedEvent(characterName, "Because you suck");
		macro.event('custom', event);
	end
end

Code: Select all

require('event/dispatcher');

include("errorhandler.lua");
include("customeventhandler.lua");
include("keypressedhandler.lua");
include("event.lua");

characterName = "Bob";
function macro.init(script, ...)
	-- The scripts default event handlers would go here.
	EventDispatcher:bindHandler('error', ErrorHandler);
	EventDispatcher:bindHandler('keypressed', KeyPressedHandler);
	EventDispatcher:bindHandler('custom', CustomEventHandler);

	-- Pretend we have some sort of auto-load mechanism for addons
	include("myerrorhandler.lua");
end

function macro.main(dt)
	
	return true;
end

function macro.event(e, ...)
	EventDispatcher:handle(e, ...);
end
Run the script, and sure enough, we see:
Bob has died. Reason: Because you suck
printed to the console (so we know CharacterDiedEvent:fire() was called) and our log file, bob.log, contains:
The time of the error was 2015-06-05 13:35:15
Bob has died. Reason: Because you suck

User avatar
lisa
Posts: 8332
Joined: Tue Nov 09, 2010 11:46 pm
Location: Australia

Re: Taking what I've learned, and applying it to MicroMacro

#4 Post by lisa » Sat Jun 06, 2015 12:31 am

Using MM 1.91.41 (latest posted on download on site)

I put event folder in scripts folder, created a main.lua in event folder with the code posted. It did a lot of errors of file missing.
I went through the files and deleted the event/ in the 'require', no errors anymore but it crashes MM after a second or 2, similar to how it used to crash for me in an infinite loop, there were no prints.

main.lua

Code: Select all

require('dispatcher');

include("errorhandler.lua");
characterName = "Bob";
function macro.init(script, ...)
   -- The scripts default event handlers would go here.
   EventDispatcher:bindHandler('error', ErrorHandler);
end

function macro.main(dt)
   error(sprintf("Some pretend error happened with the character named %s, so log it.", characterName), 1);
   return true;
end

function macro.event(e, ...)
   EventDispatcher:handle(e, ...);
end
dispatcher.lua

Code: Select all

require("handler");
local __EventDispatcher = class.new();

function __EventDispatcher:constructor()
	self.events = {};
end

function __EventDispatcher:bindHandler(eventType, handler)
	if( type(handler) ~= "table" ) then
		error("argument 2 needs to be an EventHandler", 2);
	end

	self.events[eventType] = handler;
end

function __EventDispatcher:handle(eventType, ...)
	if( self.events[eventType] ) then
		local eventHandler = self.events[eventType](...);
		return eventHandler:handle();
	end
end


EventDispatcher = __EventDispatcher();
handler.lua

Code: Select all

ErrorHandler = class.new(EventHandler);

function ErrorHandler:constructor(message)
   self.message      =   message;
end

function ErrorHandler:handle()
   local file = io.open(string.lower(characterName) .. ".log", "a");
   assert(file);
   file:write(self.message);
   file:write("\n");
   io.close();
end
Remember no matter you do in life to always have a little fun while you are at it ;)

wiki here http://www.solarstrike.net/wiki/index.php?title=Manual

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#5 Post by Administrator » Sat Jun 06, 2015 1:23 am

Those files are supposed to go into micromacro/lib (so you end up with micromacro/lib/event/dispatcher.lua and micromacro/lib/event/handler.lua), not into the scripts directory. I didn't provide the actual scripts, but instead just a quick explanation of what's going on.

The crash is because of a small bug (which I just fixed yesterday, but haven't posted the binaries yet) that happened when class.new() received nil as its only parameter (which happens when the object you specify doesn't exist, of course). It would then screw up a bunch of memory, resulting in it crashing.

I've attached a copy of my example now, as well.
Attachments
eventhandler_script_test.zip
(2.43 KiB) Downloaded 278 times

User avatar
lisa
Posts: 8332
Joined: Tue Nov 09, 2010 11:46 pm
Location: Australia

Re: Taking what I've learned, and applying it to MicroMacro

#6 Post by lisa » Sat Jun 06, 2015 2:40 am

ahh ok, all sorted now, I thought because you were talking about people doing their own additions and such the files must have been in scripts folder but I think I understand it better now. The first 2 files were for lib folder and people can do other things inside their script folder that will work with the first files instead of needing to edit the lib files themselves. Something like that anyway =)


--=== Added ===--

Ok something I am curious about, in the keypressed event handler
function KeyPressedHandler:constructor(vk, toggle)

toggle seems to be changed true and false each time a key is pressed regardless of if MM is running or not, does that mean windows keeps track of this. Seems kind of pointless to me, I can understand keeping track of key down and key up but just changing value each time it's pressed I just don't see a reason.
Maybe there is more to it I just don't know yet.

I basically just added a print(self.toggle) and then did key presses and watched how it changed.
Remember no matter you do in life to always have a little fun while you are at it ;)

wiki here http://www.solarstrike.net/wiki/index.php?title=Manual

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#7 Post by Administrator » Tue Jun 16, 2015 9:36 am

Yes, that's just something Windows does. While it seems silly for most keys (alpha-numeric keys, symbols, etc.), it has its purpose in actual toggle keys, such as Capslock. That is the only real place where the toggle field is useful.


Moving on, the next thing I wanted to talk about was further on about code separation and abstraction. I figure since I can't sleep, and I'm waiting for the boss to be around to give me some more information on my next task, I might as well write this up really quick. Bare in mind that I have not actually yet had the time to write up a real example of this working, so please just try to understand the methodology instead of put the code snippets and pseudo-code to much use.

Basically, we want to further expand on code being re-usable. Not just re-usable, but easily extendable and replaceable as well. Specifically, we're going to talk about using classes as interfaces and abstract away the sub-implementations. Why do we want to do this? So that we have more general-purpose classes with interchangeable parts, where changing a part doesn't require modification in every other location that makes use of it. Think back to the previous talk about why this is useful.

I had planned on writing up a small library for sending e-mail, as that seems like it could be quite a useful thing. I suppose many people might like to have reports, or even error messages and alerts emailed to them while they might not be at the computer, but actually writing up all the code to send an email could be a bit too much for the average user. So lets make a simple class that accepts the different required information and handles all the details for us. We might also want to allow for various drivers: SMTP, Mailgun, SES, Sendmail, and so on. While each driver may internally work a bit differently, they all accomplish the same task using the same inputs. So, first lets create the interface:

The Interface:

Code: Select all

require('mail/driver_smtp');
local _Mail =class.new();

-- Retain any information that the user might've given us.
function _Mail:construct(_from, _to, _subject, _content)
    self._driver = Driver_SMTP; -- We'll get into this shortly. Please be patient!

    self._from = from;
    self._to = to;
    self._subject = subject;
    self._content = content;
end

-- Allow us to change the driver dynamically
function _Mail:setDriver(_driver)
    self._driver = _G['Driver_' .. _driver]; -- Accept a string, like 'SMTP', and get the global variable named Driver_<whatever>
    if( not self._driver ) then
        error(sprintf("%s is not a valid mail driver.", _driver), 2);
    end
end

-- Store 'from'; return self to support method-chaining
function _Mail:from(_from)
    self._from = _from;
    return self;
end

function _Mail:to(_to)
    self._to = to;
    return self;
end

function _Mail:subject(_subject)
    self._subject = _subject;
    return self;
end

function _Mail:content(_content)
    self._content = _content;
    return self;
end

function _Mail:send()
    return self._driver:send(self); -- Pass the information from ourself off onto the driver
end


Mail = _Mail(); -- Create an instance of _Mail that is globally available under the name Mail
Now we have a class that accepts the required info (from, to, subject, and content), and will attempt to use the Driver_SMTP driver when actually sending the mail. We want to make sure that our interface is completely driver-agnostic as to not require any modification any time we need to add or modify a driver. Further, we support method chaining, so in order to send a mail, we could do something like this:

Code: Select all

local mailWasSent = Mail:from('me@noreply.com')
    :to('you.noreply.com')
    :subject('This is only a test')
    :content('Looks like the test succeeded.')
    :send();

-- Or, alternatively....
local MailWasSent = Mail('me@noreply.com', 'you@noreply.com', 'Test 2', 'Test 2 content'):send();

Now, about that driver... I won't bother with the actual socket code, so forgive the below pseudo-code. It should look something like:
The Driver

Code: Select all

require('timestamp');
-- Note: This time we aren't going to need a constructor; Everything should be passed along with the table. For this reason, we aren't going to bother with a local (declaration) and global (instantiation)

Driver_SMTP = class.new();

Driver_SMTP:connect(host, ip) -- Pretty self explanatory. Open a connection to the SMTP server
    self.connection = ...; -- Pretend stuff happens here.
end

Driver_SMTP:close() -- Close the connection
    self.connection:send('QUIT');
    self.connection = nil;
end

-- Format a piece of header
Driver_SMTP:formatHeader(field, value)
    if( value ) then
        return sprintf("%s:%s\r\n", field, value);
    else
        return sprintf("%s\r\n", field);
    end
end

-- Push all header lines to the server, one at a time.
Driver_SMTP:header(_mail)
    self.connection:send( self:formatHeader('MAIL FROM', '<' .. _mail._from .. '>') );
    self.connection:send( self:formatHeader('RCPR TO', '<' .. _mail._to .. '>') );
    self.connection:send( self.formatHeader('DATA') );

    local dataStr = self.formatHeader('Date', Timestamp:now():format("%a, %d %b %Y %T"))     -- Will look like:    Tue, 16 Jun 2015 09:14:45
        .. self.formatHeader('From', 'John Doe<' .. _mail._from .. '>')
        .. self.formatHeader('X-Mailer', 'MicroMacro SMTP Mail Driver')
        .. self.formatHeader('Replay-to', 'noreply@domain.com')
        .. self.formatHeader('X-Priority', '3') -- Normal priority
        .. self.formatHeader('To', 'John Smith<' .. _mail._to .. '>')
        .. self.formatHeader('Subject', _mail._subject)
        .. self.formatHeader('MIME Version 1.0');

    self.connection:send( dataStr );
end

-- Formats the content. To attach a file is very similar, but not going to cover that.
Driver_SMTP:formatContent(_content)
    local fmt = [[
Content-Type: multipart/mixed; boundary="__MESSAGE__ID__54yg6f6h6y456345"


--__MESSAGE__ID__54yg6f6h6y456345
Content-type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 7bit

%s
]]
    return sprintf(fmt, _content);
end

-- Push the formatted content to the server
Driver_SMTP:content(_mail)
    self.connection:send( self.formatContent(mail._content) );
end


-- Send a mail! Who would have thought?
Driver_SMTP:send(_mail)
    self:connect();
    self:header(_mail);
    self:content(_mail);
    self:close();
end
Now that was a whole lot of code, none of which I have tested, but the general idea is fairly simple: open a connection to an SMTP server, send the formatted required headers and content, and then close the connection. When we call Mail:send(), it passes the mail on to Driver_SMTP:send(), which then handles everything else for us. However, since we used an interface, it means we can totally change or rename anything (except the name of the 'send' function, in this case) without it breaking our use of the Mail class in any of our scripts. We can then move on to supporting more drivers, and again, that requires absolutely no fixing in our scripts, short of telling it which driver we might want to use. But, I'll leave that as an exercise to the reader.

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#8 Post by Administrator » Thu Jun 18, 2015 1:17 pm

I couldn't really think of a better example of interfaces last time, but I think my mail example demonstrates the idea fairly well and is a good design. For reference, here's an actual use of one (but we call them 'repositories' here; don't be confused by the different name) I have written for work:

CertificateRepository.php:

Code: Select all

<?php namespace OE\Repositories\Certificate;

interface CertificateRepository {

    public function getAll();
    public function getById($id);
    public function getAllAvailable();
    public function add($job);
    public function attachToProduct($job);
    public function edit($job);
}
And EloquentCertificateRepository.php fullfills the Eloquent driver "contract" (meaning we are supplying all the functionality that is expected by the base class):

Code: Select all

<?php namespace OE\Repositories\Certificate;

use OE\Product;
use OE\Repositories\EloquentRepository;
use OE\Certificate;

class EloquentCertificateRepository extends EloquentRepository implements CertificateRepository {

    function __construct(Certificate $model)
    {
        $this->model = $model;
    }

    function add($job)
    {
        $certificate                    =   new Certificate;
        $certificate->name              =   $job->name;
        $certificate->description       =   $job->description;
        $certificate->url               =   $job->url;
        $certificate->image_id          =   $job->imageId;

        if($certificate->save())
            return $certificate;
    }

    function attachToProduct($job)
    {
        $product = Product::findOrFail($job->productId);

        $product->certificates()->detach();


        if($job->productCertificateIds)
            $product->certificates()->sync($job->productCertificateIds);

        return $product;
    }

    function edit($job)
    {
        $certificate                    =   Certificate::findOrFail($job->id);

        if( isset($job->name) )
            $certificate->name              =   $job->name;

        if( isset($job->description) )
            $certificate->description       =   $job->description;

        if( isset($job->url) )
            $certificate->url               =   $job->url;

        if( isset($job->imageId) )
            $certificate->image_id          =   $job->imageId;

        if($certificate->save())
            return $certificate;
    }

    public function getAllAvailable()
    {
        $certificate = Certificate::all();

        return $certificate;
    }


}
And we would bind the use of CertificateRepository to EloquentCertificateRepository, such that our actual calls are going through to an Eloquent. Why do we do it this way? That way we can add another driver than Eloquent later, and since the repository is acting as an interface, we do not have to update any code in the hundreds of files we have. It "just works." And, in this specific example (a repository instead of a plain interface), we are moving the processing out of those files and into one certain location. This is useful because we can just add a function like getAllAlphabetical() to return a sorted list that is again driver-agnostic, whereas using the models directly in all those files might mean we have to totally change their code.

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#9 Post by Administrator » Thu Jun 18, 2015 1:44 pm

Next up: validators!

I think this is a fun topic. I'm not going to go over the code to make the validator class (you can download the attached file and skim over the source yourself, if you feel so inclined), but I will provide an example and an explanation of how it works. Remember that if you place validator.lua into micromacro/lib, you want to require 'validator', but if you place it in the script's directory, you want to call include('validator.lua').

Additionally, I found and fixed a minor bug with table.find() but haven't uploaded a fixed binary on that yet (and you wouldn't bother to download it just for this, anyways), so 'in' or 'type' might behave strangely at the moment.


Moving on, what's a validator? It is a class that validates user inputs. If they don't match the expected value, an error should be returned. A good example of this is in registration forms on websites: you want to validate that the username is between x and y characters, doesn't already exist, their email address is unique, their password is "good", and so on.

What we want to do is get a table of key/value pairs for the inputs; the key being the field name (such as "username") and the value being the actual user input (ie. "JohnDoe2015"). We then also provide a list of rules that should be applied to each field. Lets skip all of the technical explanation and just show how these look:

Code: Select all

	local rules = {
		something		=	'required',
		something2		=	'required',
		someNumber		=	'required|min:10',
		someNumber2		=	'required|min:10',
		someString		=	'min:1|max:10',
		someString2		=	'min:1|max:10',
		aTable			=	'type:number',
		aValue			=	'type:number,string',
		enum			=	'in:red,blue,green',
	}

	local inputs = {
		something		=	'This has a value.',
		something2		=	nil,
		someNumber		=	15,
		someNumber2		=	5,
		someString		=	'works',
		someString2		=	'This should not pass validation',
		aTable			=	{1, 2, 3},
		aValue			=	12,
		enum			=	'purple',
	};
Pretty simple. You will see that the rules are provided as a string (which will be parsed by the validator); the pipe (|) seperates the rules, and colon (:) lets us pass additional info into the rule (seperated by commas). 'required' means that the field must contain a value (any value!) and accepts nothing, while 'min' requests that the value is either (if string) not less than x characters, or (if number) not less than x, and 'max' is the same as 'min' except makes sure we don't exceed some value. 'type' is used to validate that a given field contains the Lua type expected, and 'in' validates that the field contains one of the given values.

Side note: all validator functions (except requires) should return true (meaning everything is OK) if there is no value given. Why? Because if it is required, you should be passing the required rule; everything else should still be accepted if not required.



Each rule is simply a function inside the Validator class (you can create your own sub-class with expanded rules!) that accepts the field name, the field value, and (optionally) extra parameters, and will return either true (passes validation) or false plus the error message (if it does not pass validation).

Now all we need to do to validate our inputs is to create an instance of Validator (or your sub-class, if you prefer) and call passes() on it. If it passes, then great, we can move onto the next step. If it fails, we should probably display the errors encountered:

Code: Select all

	local validator = Validator(inputs, rules);

	if( validator:passes() ) then
		print("Passed");
	else
		print("Did not pass, errors:");
		for i,v in pairs(validator:getErrors()) do
			printf("\tErr:%s\n", v);
		end
	end
And with the given example, we should receive:
Did not pass, errors:
Err:The someNumber2 field must be at least 10.
Err:The aValue field must be one of these: number, string.
Err:The aString field must be one of these: number.
Err:The someString2 field cannot be longer than 10 characters.
Err:The something2 field is required.
Err:The enum field must be one of these: red, blue, green.
*Note: Since we didn't order the tables, they can come out in any order when we use pairs(). That's why these aren't in the same order as in the code.



So, where does this become useful? One place I thought it would be absolutely great is with skill rotations. Remember how much of a pain it was to setup skills with the RoM bot? Well, why not just use a validator? Validator functions could be used to ensure that the target is in range, the skill is off cooldown, our HP is the correct percentage, or anything else you can imagine.


By the way, if you guys are still reading this, please let me know what you think of my ramblings. Does this help at all? Interesting or thought-provoking? Too wordy or overly-simplified? Please be honest.
Attachments
validator.lua
(4.54 KiB) Downloaded 271 times

User avatar
rock5
Posts: 12173
Joined: Tue Jan 05, 2010 3:30 am
Location: Australia

Re: Taking what I've learned, and applying it to MicroMacro

#10 Post by rock5 » Fri Jun 19, 2015 3:29 am

I'm going through some codecademy and codeschool courses on html, css and javascript, jquery etc., really enjoying it, so I've been a bit busy and neglecting the forum. Plus this is a bit over my head.

I understood the bit about interfaces. I think I read something like that once. The interface is sort of like a description of what the final thing is supposed to do.

I see validation as being a bit like all the eval functions in the bot. A good idea but I wonder if it's powerful enough to do the same things. I get the impression it's more geared to input validation than value evaluation. Can you do calculations and evaluations in the rules? Eg.

Code: Select all

local inputs = {
   moneyInPurse = 400,
   price = 75,
   numberWanted = 4,
}

local rules = {
   moneyInPurse = "min:math.floor(numberWanted*price)"
  • Please consider making a small donation to me to support my continued contributions to the bot and this forum. Thank you. Donate
  • I check all posts before reading PMs. So if you want a fast reply, don't PM me but post a topic instead. PM me for private or personal topics only.
  • How to: copy and paste in micromacro
    ________________________
    Quote:
    • “They say hard work never hurt anybody, but I figure, why take the chance.”
          • Ronald Reagan

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Taking what I've learned, and applying it to MicroMacro

#11 Post by Administrator » Fri Jun 19, 2015 11:24 am

Sure, but you would get the value and convert it to a string, append it to the rule:

Code: Select all

local rules = {
    moneyInPurse = 'required|min:' .. math.floor(numberWanted * price),
};
That should result in the rule: 'required|min:300'.

And here's a real-world example of us doing the exact same thing:

Code: Select all

class AddBidRequest extends Request {

    public function authorize()
    {
        return true;
    }

    public function rules()
    {

        $user = Auth::user();
        return [
            'userId'                =>  $user->id,
            'accountId'             =>  $user->account_id,
            'auctionId'             =>  'required|exists:auctions,id',
            'transactionId'         =>  '',
            'facilityId'            =>  'exists:facilities,id',
            'speciesId'             =>  'exists:species,id',
            'speciesFormatId'       =>  'exists:species_formats,id',
            'speciesSizeId'         =>  'exists:species_sizes,id',
            'speciesSizeValueId'    =>  'exists:species_sizes_values,id',
            'packageFormatId'       =>  'exists:package_formats,id',
            'packageSizeUnit'       =>  'in:lb,kg',
            'packageSizeId'         =>  'exists:package_sizes,id',
            'brandId'               =>  'exists:brands,id',
            'spec'                  =>  'in:value added,salted,pickled,smoked,live,frozen,fresh',
            'price'                 =>  'required|numeric|min:1|max:1000000',
            'volume'                =>  'required|integer|min:1|max:1000000',
            'status'                =>  'in:open,matched,accepted,closed',
            'confirmed'             =>  'integer',
            'expiresAt'             =>  'date_format:' . Config::get('time.inputDateFormat'),
        ];
    }
}
In PHP, just one dot (.) is used for concatenation instead of two (..) like in Lua. You're also noticing that it is in a class, and named 'Request'. That's for a good reason; I'll get into command-busses eventually, but I thought it good to cover these other things first.


And finally, you could manually call one specific validator function however you please:

Code: Select all

passed,errMsg = Validator:min('moneyInPurse', moneyInPurse, math.floor(price * numberWanted));
The first field to these functions ('moneyInPurse' in this example) is just a string of the fieldname, and is only used to format the error message they will return.




You could also do this:

Code: Select all

PurseValidator = class.new(Validator);

function PurseValidator:minMoney(field, purseObj)
    local minAccepted = math.floor(purseObj.price * purseObj.numberWanted);
    return self:min(field, purseObj.moneyInPurse, minAccepted);
end

Code: Select all

   moneyInPurse = 'required|minMoney',  -- Just make sure you're using a PurseValidator instead of regular Validator.

User avatar
rock5
Posts: 12173
Joined: Tue Jan 05, 2010 3:30 am
Location: Australia

Re: Taking what I've learned, and applying it to MicroMacro

#12 Post by rock5 » Fri Jun 19, 2015 11:40 am

Administrator wrote:

Code: Select all

local rules = {
    moneyInPurse = 'required|min:' .. math.floor(numberWanted * price),
};
Doh! Didn't think of that.
  • Please consider making a small donation to me to support my continued contributions to the bot and this forum. Thank you. Donate
  • I check all posts before reading PMs. So if you want a fast reply, don't PM me but post a topic instead. PM me for private or personal topics only.
  • How to: copy and paste in micromacro
    ________________________
    Quote:
    • “They say hard work never hurt anybody, but I figure, why take the chance.”
          • Ronald Reagan

Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests