AASM: Separating the state machine definition from the class definition - ruby

suppose I have this class (taken directly from the aasm documentation):
class Job < ActiveRecord::Base
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => [:running, :cleaning], :to => :sleeping
end
end
end
I don't like a lot the fact that I have my state machine definition mixed with my class definition (since of course in a real project I will add more methods to the Job class).
I would like to separate the state machine definition in a module so that the Job class can be something along the line of:
class Job < ActiveRecord::Base
include StateMachines::JobStateMachine
end
I then created a job_state_machine.rb file in app/models/state_machines with a content similar to:
module StateMachines::JobStateMachine
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => [:running, :cleaning], :to => :sleeping
end
end
end
but this is not working because AASM is being included in the module not in the Job class... I even tried changing the module to:
module StateMachines::JobStateMachine
def self.included(base)
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => [:running, :cleaning], :to => :sleeping
end
end
end
end
but still it is not working... any hint or suggestion is very appreciated.
Thanks,
Ignazio
EDIT:
Thanks to Alto, the correct solution is this:
module StateMachine::JobStateMachine
def self.included(base)
base.send(:include, AASM)
base.send(:aasm, column: 'status') do
....
end
end
end
and obviously remember to include the state machine definition in the main class like this:
include StateMachine::JobStateMachine

Couldn't you simple do this?
module StateMachines::JobStateMachine
def self.included(base)
base.send(:include, AASM)
aasm do
...
end
end
end

Related

Share validations across models with rails 4

There are other questions pertaining to this, but they all seems to be < Rails 4, and what's more, they're not too detailed!
They all talk about creating a module to share these common validations in:
require 'active_record'
module ContactValidations
def self.included(base_class)
base_class.class_eval do
include ContactValidations::InstanceMethods
# model validations
validates_presence_of(:name, :message => 'You must provide a company name.')
validates_presence_of(:street, :message => 'You must provide a company street.')
validates_presence_of(:city, :message => 'You must provide a company city.')
validates_presence_of(:post_code, :message => 'You must provide a company post code.')
validates_numericality_of(:telephone, :on => :create, :message => 'Telephone should be a number with no spaces.', :if => :telephone_given?)
validates_numericality_of(:area_code, :on => :create, :message => 'Telephone area code should be a number with no spaces.', :if => :area_code_given?)
validates_numericality_of(:fax, :on => :create, :message => 'Fax should be a number with no spaces.', :if => :fax_given?)
validates_numericality_of(:fax_area_code, :on => :create, :message => 'Fax area code should be a number with no spaces.', :if => :fax_area_code_given?)
validates_format_of(:web, :with => /^((http)?:\/\/)?(www\.)?([a-zA-Z0-9]+)(.[a-zA-Z0-9]{2,3})(\.[a-zA-Z0-9]{2,3})$/, :on => :create, :message => 'Web address is invalid. Example: http://www.domain or http://domain.', :if => :web_given?)
validates_format_of(:email, :with => /^([a-zA-Z0-9_\-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/, :on => :create, :message => 'Th email address given is invalid.', :if => :email_given?)
validates_uniqueness_of(:email, :message => 'This email is already given.')
end
end
module InstanceMethods
def telephone_given?
!telephone.blank?
end
def fax_given?
!fax.blank?
end
def web_given?
!web.blank?
end
def email_given?
!email.blank?
end
def area_code_given?
!area_code.blank?
end
def fax_area_code_given?
!fax_area_code.blank?
end
end
end
But I for one have no idea where such a file should be saved. In the lib directory? All files in the lib directory are included, seems a bit wasteful for a module I only want to be included in a two or three models...
Does Rails 4 have an inbuilt way to share validations?
If not, where should I save my custom module?
And just to be super clear, how should I require this module in the models that need to include these validations?
Create the module in app/models/concerns, then include them in your classes with:
include ContactValidations
In this way Rails will automatically load the shared modules and make them available for you to include.
you should also have a look at validates_with
https://api.rubyonrails.org/v6.0.3.3/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates_with

cramp framework sync 'render' correct way using em-synchrony

To describe my problem I attach simple Cramp http://cramp.in/ class.
I add some modification but its mainly work like https://github.com/lifo/cramp-pub-sub-chat-demo/blob/master/app/actions/chat_action.rb
class ChatAction < Cramp::Websocket
use_fiber_pool
on_start :create_redis
on_finish :handle_leave, :destroy_redis
on_data :received_data
def create_redis
#redis = EM::Hiredis.connect('redis://127.0.0.1:6379/0')
end
def destroy_redis
#redis.pubsub.close_connection
#redis.close_connection
end
def received_data(data)
msg = parse_json(data)
case msg[:action]
when 'join'
handle_join(msg)
when 'message'
handle_message(msg)
else
# skip
end
end
def handle_join(msg)
#user = msg[:user]
subscribe
publish(:action => 'control', :user => #user, :message => 'joined the chat room')
end
def handle_leave
publish :action => 'control', :user => #user, :message => 'left the chat room'
end
def handle_message(msg)
publish(msg.merge(:user => #user))
# added only for inline sync tests
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
private
def subscribe
#redis.pubsub.subscribe('chat') do |message|
render(message)
end
end
def publish(message)
#redis.publish('chat', encode_json(message))
end
def encode_json(obj)
Yajl::Encoder.encode(obj)
end
def parse_json(str)
Yajl::Parser.parse(str, :symbolize_keys => true)
end
def render_json(hash)
render encode_json(hash)
end
end
More about what i try to do is in handle_message method.
I try send messages to client in correct order. First publish message to all subscribers, second render some internal info only for current connected client.
For above code client receives:
{"action":"message","user":"user1","message":"this info should appear after published message"}
{"action":"message","message":"simple message","user":"user1"}
Its not synchronized, because of em-hiredis defferable responses, probably.
So I try to synchronized it this way:
def handle_message(msg)
EM::Synchrony.sync publish(msg.merge(:user => #user))
EM::Synchrony.next_tick do # if I comment this block messages order is still incorrect
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
end
Now, client handle messages with correct order.
{"action":"message","message":"simple message","user":"user1"}
{"action":"message","user":"user1","message":"this info should appear after published message"}
My questions are:
When I comment EM::Synchrony.next_tick block, messages order is still incorrect. What meaning have EM::Synchrony.next_tick block in this example?
Is this good way to handle inline sync with Cramp or EventMachine ?
Is there a better, clearer way to handle it ?
Thank you!
I found solution of this problem, em-synchrony should work inline out of the box by requiring this library:
require 'em-synchrony/em-hiredis'
class ChatAction < Cramp::Websocket
Using EM::Synchrony.next_tick block is bad idea, with big help of em-synchrony community I add em-hiredis 0.2.1 compatibility patch on github
So now handle_message method looks like this:
def handle_message(msg)
publish(msg.merge(:user => #user))
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
Don`t forget to take this gem from github
gem 'em-synchrony', :git=> 'git://github.com/igrigorik/em-synchrony.git'
Hope it helps someone.

Resque worker gives out "NoMethodError: undefined method `perform`"

I have no idea what I have done here, but I have attempted to get one controller in Rails to queue a job onto Resque, which then a worker connects to and does the heavy lifting (I.E. comparisons, database entries).
However, the tasks are not even running, since there are no clear instructions for setting Resque up.
Copy and paste's below:
Also available in Gist format!
This is the exception line from Hoptoad:
NoMethodError: undefined method 'perform' for Violateq:Module
This is the contents of the "worker" file:
module Violateq
#queue = :violateq
def perform(nick, rulenumber)
# Working for the weekend!!!
puts "I got a nick of #{nick} and they broke #{rulenumber}"
#violation = Violation.new(nick, rulenumber)
puts "If you got this far, your OK"
log_in(:worker_log, {:action => "Violate d=perfom", :nick => nick, :rulenumber => rulenumber, :status => "success"})
#rescue => ex
# notify_hoptoad(ex)
# log_in(:worker_log, {:action => "Violate d=perfom", :nick => nick, :rulenumber => rulenumber, :status => "failure"})
end
end
This is the contents of the "web_controller" file:
class IncomingController < ApplicationController
require 'mail'
skip_before_filter :verify_authenticity_token
def create
message = Mail.new(params[:message])
# Push the message into the queue
Resque.enqueue(Violateq, message.from.to_s, message.subject.to_s)
log_in(:endpoint_log, {:action => "IncomingController d=create", :subject => message.subject, :message => message.body.decoded})
render :text => 'success', :status => 200 # a status of 404 would reject the mail
rescue => ex
notify_hoptoad(ex)
render :text => 'failure', :status => 500
end
end
Thank you very much for your time, and if you would like any more information, please do not hesitate to contact me,
Luke Carpenter
Fixed.
Changed def perform to def self.perform
Then it worked
Thanks,
Luke Carpenter

Dynamic State Machine in Ruby? Do State Machines Have to be Classes?

Question is, are state machines always defined statically (on classes)? Or is there a way for me to have it so each instance of the class with has it's own set of states?
I'm checking out Stonepath for implementing a Task Engine. I don't really see the distinction between "states" and "tasks" in there, so I'm thinking I could just map a Task directly to a state. This would allow me to be able to define task-lists (or workflows) dynamically, without having to do things like:
aasm_event :evaluate do
transitions :to => :in_evaluation, :from => :pending
end
aasm_event :accept do
transitions :to => :accepted, :from => :pending
end
aasm_event :reject do
transitions :to => :rejected, :from => :pending
end
Instead, a WorkItem (the main workflow/task manager model), would just have many tasks. Then the tasks would work like states, so I could do something like this:
aasm_initial_state :initial
tasks.each do |task|
aasm_state task.name.to_sym
end
previous_state = nil
tasks.each do |tasks|
aasm_event task.name.to_sym do
transitions :to => "#{task.name}_phase".to_sym, :from => previous_state ? "#{task.name}_phase" : "initial"
end
previous_state = state
end
However, I can't do that with the aasm gem because those methods (aasm_state and aasm_event) are class methods, so every instance of the class with that state machine has the same states. I want it so a "WorkItem" or "TaskList" dynmically creates a sequence of states and transitions based on the tasks it has.
This would allow me to dynamically define workflows and just have states map to tasks.
Are state machines ever used like this? It seems that this ruby workflow gem is similar to what I'm describing.
Update: I can see doing something like the following, but it seems sort of hackish:
#implementation_state_machine = Class::new do
include AASM
aasm_initial_state :initial
tasks.each { |state| aasm_state :"#{task.name}"}
# ...
end
... where a property on my model would be implementation_state_machine. I'd have to override method_missing to delegate state-related methods (accepted_phase?) to the implementation anonymous class.
Yeah, that does seem very hacky and quite messy. I wrote a new gem recently that allows you to use dynamic 'to' transitions with a decision setting.
So instead of building your events and transitions dynamically, would be it be possible to map them out first, and use the decide setting to allow the transition decide which new state to enter? You can also wrap your from transition in an array so you wouldn't need to do :from => previous_state ? "#{task.name}_phase" : "initial", you could just do :from => [ :cool_task_phase, :initial ]
I find that setting out your transitions and events out first, allows you to get a greater picture on what your model is doing.
Check it out at http://github.com/ryanza/stateflow
Hopefully you can find some use out of this.
In my implementation state machine is a hash https://github.com/mpapis/state_attr
state_attr :state, {
nil => :first,
:first => [:second, :third],
:second => :last,
:third => nil,
}
you can define as many state attributes as you like
BTW: in the background there is still a class but only as a proxy to attribute

Can I make AASM run a specific method on event fail?

Is there a nice way to tell AASM that if an exception is raised while processing any assm_event I want that error to be caught by a specific block of code?
eg currently I do something like
assm_state :state_1
assm_state :state_2, :before_enter => :validate_something
assm_state :failed
assm_event :something_risky do
transition :from => :state_1, :to => :state_2
end
assm_event :fail do
transition :from => [:state_1, :state_2], :to => :failed
end
def validate_something
begin
something_that_might_raise_error
rescue
self.record_error
self.fail
end
end
and what I would prefer to do is something like
assm_state :state_1
assm_state :state_2, :before_enter => :validate_something
assm_state :failed
assm_event :something_risky, :on_exception => :log_failure do
transition :from => :state_1, :to => :state_2
end
assm_event :fail do
transition :from => [:state_1, :state_2], :to => :failed
end
def validate_something
something_that_might_raise_exception
end
def log_failure
self.record_error
self.fail
end
and have log_failure be called if something_that_might_raise_exception does raise an exception. Ideally I want to avoid changing AASM so I am safe if I need to upgrade it in the future
If you use SimpleStateMachine you can do:
def something_risky
something_that_might_raise_error
rescue
record_error
raise #reraise the error
end
event :something_risky, :state1 => :state2,
RuntimeError => :failed
I also had this problem. I had two things to do.
Follow the suggestion in this blog http://degenportnoy.blogspot.com/2009/11/event-callbacks-in-aasm.html.
You need to restart your app even if you are working on development mode.

Resources