### Chaining Ohm Sets

#### Doing the straight forward approach

# Let's design our example around the following requirements:
#
# 1. a `User` has many orders.
# 2. an  `Order` can be pending, authorized or captured.
# 3. a `Product` is referenced by an `Order`.

#### Doing it the normal way

# Let's first require `Ohm`.
require "ohm"

# A `User` has a `collection` of *orders*. Note that a collection
# is actually just a convenience, which implemented simply will look like:
#
#     def orders
#       Order.find(:user_id => self.id)
#     end
#
class User < Ohm::Model
  collection :orders, Order
end

# The product for our purposes will only contain a name.
class Product < Ohm::Model
  attribute :name
end

# We define an `Order` with just a single `attribute` called state, and
# also add an `index` so we can search an order given its state.
#
# The `reference` to the `User` is actually required for the `collection`
# of *orders* in the `User` declared above, because the `reference` defines
# an index called `:user_id`.
#
# We also define a `reference` to a `Product`.
class Order < Ohm::Model
  attribute :state
  index :state

  reference :user, User
  reference :product, Product
end

##### Testing what we have so far.

# For the purposes of this tutorial, we'll use cutest for our test framework.
require "cutest"

# Make sure that every run of our test suite has a clean Redis instance.
prepare { Ohm.flush }

# Let's create a *user*, a *pending*, *authorized* and a captured order.
# We also create two products named *iPod* and *iPad*.
setup do
  @user = User.create

  @ipod = Product.create(:name => "iPod")
  @ipad = Product.create(:name => "iPad")

  @pending =    Order.create(:user => @user, :state => "pending",
                             :product => @ipod)
  @authorized = Order.create(:user => @user, :state => "authorized",
                             :product => @ipad)
  @captured =   Order.create(:user => @user, :state => "captured",
                             :product => @ipad)
end

# Now let's try and grab all pending orders, and also pending
# *iPad* and *iPod* ones.
test "finding pending orders" do
  assert @user.orders.find(state: "pending").include?(@pending)

  assert @user.orders.find(:state => "pending",
                           :product_id => @ipod.id).include?(@pending)

  assert @user.orders.find(:state => "pending",
                           :product_id => @ipad.id).empty?
end

# Now we try and find captured and authorized orders. The tricky part
# is trying to find an order that is either *captured* or *authorized*,
# since `Ohm` as of this writing doesn't support unions in its
# finder syntax.
test "finding authorized and/or captured orders" do
  assert @user.orders.find(:state => "authorized").include?(@authorized)
  assert @user.orders.find(:state => "captured").include?(@captured)

  assert @user.orders.find(:state => ["authorized", "captured"]).empty?

  auth_or_capt = @user.orders.key.volatile[:auth_or_capt]
  auth_or_capt.sunionstore(
    @user.orders.find(:state => "authorized").key,
    @user.orders.find(:state => "captured").key
  )

  assert auth_or_capt.smembers.include?(@authorized.id)
  assert auth_or_capt.smembers.include?(@captured.id)
end

#### Creating shortcuts

# You can of course define methods to make that code more readable.
class User < Ohm::Model
  def authorized_orders
    orders.find(:state => "authorized")
  end

  def captured_orders
    orders.find(:state => "captured")
  end
end

# And we can now test these new methods.
test "finding authorized and/or captured orders" do
  assert @user.authorized_orders.include?(@authorized)
  assert @user.captured_orders.include?(@captured)
end

# In most cases this is fine, but if you want to have a little fun,
# then we can play around with some chainability.

#### Chaining Kung-Fu

# The `Ohm::Model::Set` takes a *Redis* key and a *class monad*
# for its arguments.
#
# We can simply subclass it and define the monad to always be an
# `Order` so we don't have to manually set it everytime.
class UserOrders < Ohm::Model::Set
  def initialize(key)
    super key, Ohm::Model::Wrapper.wrap(Order)
  end

  # Here is the crux of the chaining pattern. Instead of
  # just doing a straight up `find(:state => "pending")`, we return
  # `UserOrders` again.
  def pending
    self.class.new(model.index_key_for(:state, "pending"))
  end

  def authorized
    self.class.new(model.index_key_for(:state, "authorized"))
  end

  def captured
    self.class.new(model.index_key_for(:state, "captured"))
  end

  # Now we wrap the implementation of doing an `SUNIONSTORE` and also
  # make it return a `UserOrders` object.
  #
  # NOTE: `volatile` just returns the key prepended with a `~:`, so in
  # this case it would be `~:Order:accepted`.
  def accepted
    model.key.volatile[:accepted].sunionstore(
      authorized.key, captured.key
    )

    self.class.new(model.key.volatile[:accepted])
  end
end

# Now let's re-open the `User` class and add a customized `orders` method.
class User < Ohm::Model
  def orders
    UserOrders.new(Order.index_key_for(:user_id, id))
  end
end

# Ok! Let's put all of that chaining code to good use.
test "finding pending orders using a chainable style" do
  assert @user.orders.pending.include?(@pending)
  assert @user.orders.pending.find(:product_id => @ipod.id).include?(@pending)

  assert @user.orders.pending.find(:product_id => @ipad.id).empty?
end

test "finding authorized and/or captured orders using a chainable style" do
  assert @user.orders.authorized.include?(@authorized)
  assert @user.orders.captured.include?(@captured)

  assert @user.orders.accepted.include?(@authorized)
  assert @user.orders.accepted.include?(@captured)

  accepted = @user.orders.accepted

  assert accepted.find(:product_id => @ipad.id).include?(@authorized)
  assert accepted.find(:product_id => @ipad.id).include?(@captured)
end

#### Conclusion

# This design pattern is something that really depends upon the situation. In
# the example above, you can add more complicated querying on the `UserOrders`
# class.
#
# The most important takeaway here is the ease of which we can weild the
# different components of Ohm, and mold it accordingly to our preferences,
# without having to monkey-patch anything.