Skip to content

Commit

Permalink
Merge pull request #370 from gocardless/revert-358-cleaner-query-mixin
Browse files Browse the repository at this point in the history
Revert "Cleaner ActiveRecordQueries mixin"
  • Loading branch information
danwakefield authored Nov 11, 2019
2 parents b7e1a9c + 8459ba4 commit d163911
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 253 deletions.
59 changes: 37 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,22 @@ Then, link it to your model:

```ruby
class Order < ActiveRecord::Base
has_many :order_transitions, autosave: false
include Statesman::Adapters::ActiveRecordQueries

include Statesman::Adapters::ActiveRecordQueries[
transition_class: OrderTransition,
initial_state: :pending
]
has_many :order_transitions, autosave: false

def state_machine
@state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
end

def self.transition_class
OrderTransition
end

def self.initial_state
:pending
end
private_class_method :initial_state
end
```

Expand Down Expand Up @@ -351,34 +357,43 @@ callback code throws an exception, it will not be caught.)

A mixin is provided for the ActiveRecord adapter which adds scopes to easily
find all models currently in (or not in) a given state. Include it into your
model and passing in `transition_class` and `initial_state` as options.

In 4.1.1 and below, these two options had to be defined as methods on the model,
but 4.2.0 and above allow this style of configuration as well. The old method
pollutes the model with extra class methods, and is deprecated, to be removed
in 5.0.0.
model and define `transition_class` and `initial_state` class methods:

```ruby
class Order < ActiveRecord::Base
has_many :order_transitions, autosave: false
include Statesman::Adapters::ActiveRecordQueries[
transition_class: OrderTransition,
initial_state: OrderStateMachine.initial_state
]
include Statesman::Adapters::ActiveRecordQueries

def self.transition_class
OrderTransition
end
private_class_method :transition_class

def self.initial_state
OrderStateMachine.initial_state
end
private_class_method :initial_state
end
```

If the transition class-name differs from the association name, you will also
need to pass `transition_name` as an option:
need to define a corresponding `transition_name` class method:

```ruby
class Order < ActiveRecord::Base
has_many :transitions, class_name: "OrderTransition", autosave: false
include Statesman::Adapters::ActiveRecordQueries[
transition_class: OrderTransition,
initial_state: OrderStateMachine.initial_state,
transition_name: :transitions
]

def self.transition_name
:transitions
end

def self.transition_class
OrderTransition
end

def self.initial_state
OrderStateMachine.initial_state
end
private_class_method :initial_state
end
```

Expand Down
130 changes: 34 additions & 96 deletions lib/statesman/adapters/active_record_queries.rb
Original file line number Diff line number Diff line change
@@ -1,122 +1,51 @@
module Statesman
module Adapters
module ActiveRecordQueries
def self.check_missing_methods!(base)
missing_methods = %i[transition_class initial_state].
reject { |_method| base.respond_to?(:method) }
return if missing_methods.none?

raise NotImplementedError,
"#{missing_methods.join(', ')} method(s) should be defined on " \
"the model. Alternatively, use the new form of `extend " \
"Statesman::Adapters::ActiveRecordQueries[" \
"transition_class: MyTransition, " \
"initial_state: :some_state]`"
end

def self.included(base)
check_missing_methods!(base)

base.include(
ClassMethods.new(
transition_class: base.transition_class,
initial_state: base.initial_state,
most_recent_transition_alias: base.try(:most_recent_transition_alias),
transition_name: base.try(:transition_name),
),
)
base.extend(ClassMethods)
end

def self.[](**args)
ClassMethods.new(**args)
end

class ClassMethods < Module
def initialize(**args)
@args = args
end

def included(base)
ensure_inheritance(base)

query_builder = QueryBuilder.new(base, **@args)

base.define_singleton_method(:most_recent_transition_join) do
query_builder.most_recent_transition_join
end

define_in_state(base, query_builder)
define_not_in_state(base, query_builder)
end
module ClassMethods
def in_state(*states)
states = states.flatten.map(&:to_s)

private

def ensure_inheritance(base)
klass = self
existing_inherited = base.method(:inherited)
base.define_singleton_method(:inherited) do |subclass|
existing_inherited.call(subclass)
subclass.send(:include, klass)
end
joins(most_recent_transition_join).
where(states_where(most_recent_transition_alias, states), states)
end

def define_in_state(base, query_builder)
base.define_singleton_method(:in_state) do |*states|
states = states.flatten.map(&:to_s)
def not_in_state(*states)
states = states.flatten.map(&:to_s)

joins(most_recent_transition_join).
where(query_builder.states_where(states), states)
end
end

def define_not_in_state(base, query_builder)
base.define_singleton_method(:not_in_state) do |*states|
states = states.flatten.map(&:to_s)

joins(most_recent_transition_join).
where("NOT (#{query_builder.states_where(states)})", states)
end
end
end

class QueryBuilder
def initialize(model, transition_class:, initial_state:,
most_recent_transition_alias: nil,
transition_name: nil)
@model = model
@transition_class = transition_class
@initial_state = initial_state
@most_recent_transition_alias = most_recent_transition_alias
@transition_name = transition_name
end

def states_where(states)
if initial_state.to_s.in?(states.map(&:to_s))
"#{most_recent_transition_alias}.to_state IN (?) OR " \
"#{most_recent_transition_alias}.to_state IS NULL"
else
"#{most_recent_transition_alias}.to_state IN (?) AND " \
"#{most_recent_transition_alias}.to_state IS NOT NULL"
end
joins(most_recent_transition_join).
where("NOT (#{states_where(most_recent_transition_alias, states)})",
states)
end

def most_recent_transition_join
"LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias}
ON #{model.table_name}.id =
ON #{table_name}.id =
#{most_recent_transition_alias}.#{model_foreign_key}
AND #{most_recent_transition_alias}.most_recent = #{db_true}"
end

private

attr_reader :model, :transition_class, :initial_state
def transition_class
raise NotImplementedError, "A transition_class method should be " \
"defined on the model"
end

def initial_state
raise NotImplementedError, "An initial_state method should be " \
"defined on the model"
end

def transition_name
@transition_name || transition_class.table_name.to_sym
transition_class.table_name.to_sym
end

def transition_reflection
model.reflect_on_all_associations(:has_many).each do |value|
reflect_on_all_associations(:has_many).each do |value|
return value if value.klass == transition_class
end

Expand All @@ -133,9 +62,18 @@ def model_table
transition_reflection.table_name
end

def states_where(temporary_table_name, states)
if initial_state.to_s.in?(states.map(&:to_s))
"#{temporary_table_name}.to_state IN (?) OR " \
"#{temporary_table_name}.to_state IS NULL"
else
"#{temporary_table_name}.to_state IN (?) AND " \
"#{temporary_table_name}.to_state IS NOT NULL"
end
end

def most_recent_transition_alias
@most_recent_transition_alias ||
"most_recent_#{transition_name.to_s.singularize}"
"most_recent_#{transition_name.to_s.singularize}"
end

def db_true
Expand Down
Loading

0 comments on commit d163911

Please sign in to comment.