condi 0.0.1 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  MIT LICENSE
2
2
  from http://www.opensource.org/licenses/mit-license.php
3
3
 
4
- Copyright (C) 2011 by Larry Kyrala
4
+ Copyright (C) 2012 by Larry Kyrala
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -4,16 +4,25 @@ Condi
4
4
 
5
5
  Condi is a gem that you use with Rails to make it easier to cleanly implement conditional elements in a view.
6
6
 
7
- Condi allows you to define predicates in the controller that are callable in the view without relying on unneeded instance variables or business logic in the views. Because the predicates are defined dynamically during a controller action, they are easy to find and easy to use without hopping around multiple files.
7
+ Condi allows you to define boolean-valued *predicates* in the controller that are callable in the view without relying on unneeded instance variables or business logic in the views. Because the predicates are defined dynamically during a controller action, they are easy to find and easy to use without hopping around multiple files.
8
+
9
+ Condi also allows definition of *synonyms* that can return any value (not just booleans).
10
+
8
11
 
9
12
  API Doc: {Condi}
10
13
 
11
- Copyright (C) 2011 by Larry Kyrala. MIT Licensed.
14
+ Copyright (C) 2012 by Larry Kyrala. MIT Licensed.
15
+
16
+ Installation
17
+ ------------
12
18
 
13
- Example
14
- -------
19
+ gem install condi
15
20
 
16
- For example, say you have a User who has various roles and a shopping Cart that contains items. Your StoreController loads the current User and Cart objects, and your store view displays the User and Cart information. But let's say that orders over a certain amount for new customers should show a "free ground shipping" option. With Condi you can do the following:
21
+
22
+ A Simple Example
23
+ ----------------
24
+
25
+ For example, say you have a User who has various roles and a shopping Cart that contains items. Your StoreController loads the current User and Cart objects, and your store view displays the User and Cart information. But let's say that orders over a certain amount for new customers should show a "free ground shipping" option. With Condi you can define a `predicate` that does the following:
17
26
 
18
27
  `app/controllers/store_controller.rb:`
19
28
 
@@ -35,6 +44,77 @@ For example, say you have a User who has various roles and a shopping Cart that
35
44
  <% end %>
36
45
  <% end %>
37
46
 
47
+
48
+ Predicates with Arguments
49
+ -------------------------
50
+
51
+ Say you would like to monitor your cart status and highlight items that are shipped but haven't arrived yet. You would like to hand the collection of items off to a partial, but how can you use
52
+ a predicate in this situation? You need a predicate with an argument!
53
+
54
+ `app/controllers/store_controller.rb:`
55
+
56
+ class StoreController
57
+ include Condi
58
+
59
+ def items
60
+ cart = Cart.find(cart_id)
61
+ @items = cart.items
62
+ predicate(:shipping?) { |item| item.status == :shipped && DeliveryService.status(item.tracking_number) !~ /arrived/ }
63
+ end
64
+ end
65
+
66
+ `app/views/store/items.html.erb:`
67
+
68
+ <table>
69
+ <%= render :partial => "item", :collection => @items %>
70
+ </table>
71
+
72
+ `app/views/store/_item.html.erb:`
73
+
74
+ <% if shipping?(item) %>
75
+ <tr class="shipping">
76
+ <% else %>
77
+ <tr>
78
+ <% end %>
79
+ <td><%= item.to_s %></td>
80
+ </tr>
81
+
82
+
83
+ Synonyms: defining blocks that return non-boolean values
84
+ --------------------------------------------------------
85
+
86
+ Although the previous example works, wouldn't it be nicer if we could simply define a `synonym` that
87
+ returns the css class we need for a given item?
88
+
89
+ `app/controllers/store_controller.rb:`
90
+
91
+ class StoreController
92
+ include Condi
93
+
94
+ def items
95
+ cart = Cart.find(cart_id)
96
+ @items = cart.items
97
+ synonym(:css_for_item_status) do |item|
98
+ if item.status == :shipped
99
+ if DeliveryService.status(item.tracking_number) !~ /arrived/
100
+ "shipping"
101
+ else
102
+ "shipped"
103
+ end
104
+ else
105
+ "processing"
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ `app/views/store/_item.html.erb:`
112
+
113
+ <tr class="<%= css_for_item_status %>">
114
+ <td><%= item.to_s %></td>
115
+ </tr>
116
+
117
+
38
118
  The Problem
39
119
  -----------
40
120
 
@@ -69,7 +149,7 @@ Not the cleanest approach since business logic is in our views now. What's anot
69
149
  <% end %>
70
150
  <% end %>
71
151
 
72
- We haven't gained much except shuffling arguments around.
152
+ We haven't gained much except shuffling arguments around and we're coupling Carts with Users unnecessarily.
73
153
 
74
154
 
75
155
  Or, we could put the predicate in a helper and remove the args:
@@ -117,14 +197,37 @@ How does this work? Behind the scenes, `predicate` defines an instance method o
117
197
  Advantages
118
198
  ----------
119
199
 
120
- * Because predicates are closures, there is less of a chance that helpers and controllers and views get "mixed up" -- i.e. someone copies some code from one view to another but forgets to set the correct instance variables in the controller.
200
+ * Because predicates are closures, you only have to worry about setting context once in the controller action instead of coordinating context across multiple files. This keeps your views functional and makes them easier to refactor without breaking existing business logic.
201
+
202
+ * Placing predicates in the Controller allows them to orchestrate multiple Models without breaking encapsulation between Models. The Controller is arguably a better place to define such predicates from an MVC perspective.
203
+
204
+ * Condi makes it simple to define predicates and synonyms in the Controller and call them from the view without cluttering the helper namespace and creating a maze of unique names for every action. Condi is more flexible.
205
+
206
+ * Another advantage of the predicate being defined dynamically on the Controller is that the predicate can never be inadvertently called as an action itself. Condi offers better encapsulation of state.
207
+
208
+ * Condi works with Rails 2 and Rails 3 apps equally well.
209
+
210
+
211
+ Disadvantages
212
+ -------------
213
+
214
+ * Controllers may become "thicker" than you might like.
215
+
216
+ * It may be awkward to share predicates across multiple actions. But if you think about it, it is awkward to share context as well. Filters are a common solution for both problems. You can define your predicates in a before_filter method to ensure that both the context and the predicates will be sharable.
217
+
218
+
219
+ Background
220
+ ----------
121
221
 
122
- * Also, the Model shouldn't define such predicates, because they control behavior in the view, not to mention that predicates can orchestrate several Models together with business logic. The Controller is arguably a better place to define such predicates from an MVC perspective.
222
+ Condi is something I wanted to explore as a response to the general problem of implementing *business logic* and rules in Rails apps.
123
223
 
124
- * Condi makes it simple to define predicates in the Controller and call them from the view without cluttering the helper namespace and creating a maze of unique names for every action. Condi is more flexible.
224
+ * Rules-engines exist (i.e. `rools`), but are heavier-weight than Condi and can be complex to use.
125
225
 
126
- * Another important advantage of the predicate being defined as an instance method on the Controller during an action-view execution is that the predicate method will never exist on the Controller for subsequent actions -- hence the predicate can never be inadvertently called as an action itself.
226
+ * Rails `cells` is a promising component framework, but is also on the heavy side and may not fit well with legacy apps.
127
227
 
228
+ * Render-partial locals are a decent lighter-weight alternative to `cells`, but can sometimes be tricky to keep consistent across multiple controller action contexts.
128
229
 
230
+ * Doing the "right thing" isn't always an option in business logic. If a customer wants to pay you a ton of money but your business model doesn't support it, are you going to refuse the money or change your business model?
129
231
 
232
+ * If business logic turns into a mess, at least we can strive to contain the rules in one place/context so that we can identify which problems are due to inconsistency in the business rules and which are due to coding errors.
130
233
 
data/lib/condi.rb CHANGED
@@ -1,22 +1,40 @@
1
- # Include this module in an ActionController to define predicates within an action that
2
- # can be used later in the related action view. For example:
1
+ # Include this module in an ActionController to define predicates or synonyms within an action context that
2
+ # can be used later in the related action view.
3
+ # @example
3
4
  # class StoreController
4
5
  # include Condi
5
6
  # ...
6
7
  # end
7
8
  module Condi
8
9
 
9
- # define a predicate (instance method) on the controller which is callable from the related view.
10
+ # define a method on the controller which is callable from the related view.
10
11
  # @example define a predicate that determines whether or not to show a "free shipping" option.
11
- # predicate(:show_free_shipping?) { user.new_customer? && cart.amount > 100 }
12
- # @param [Symbol] method_name name of the predicate method. (e.g. :show_action_button?)
13
- # @param [Proc] block {} or do...end block that evaluates to true or false.
14
- # @note You are not required to end your method name in a question mark, however it is conventional to do so.
15
- # @see the full example in the README
16
- def predicate(method_name, &block)
17
- self.class.instance_eval do
18
- define_method(method_name, &block)
19
- helper_method(method_name)
20
- end
21
- end
12
+ # predicate(:show_free_shipping?) { user.new_customer? && cart.amount > 100 }
13
+ # @example define a predicate that takes an element of a collection as an argument.
14
+ # predicate(:shipping?) { |item| item.status == :shipped && DeliveryService.status(item.tracking_number) !~ /arrived/ }
15
+ # @example define a synonym that returns a css class based on item status
16
+ # synonym(:css_for_item_status) do |item|
17
+ # if item.status == :shipped
18
+ # if DeliveryService.status(item.tracking_number) !~ /arrived/
19
+ # "shipping"
20
+ # else
21
+ # "shipped"
22
+ # end
23
+ # else
24
+ # "processing"
25
+ # end
26
+ # end
27
+ # @param [Symbol] method_name name of the predicate or synonym method. (e.g. :show_action_button?)
28
+ # @param [Proc] block {} or do...end block.
29
+ # @note You are not required to end a predicate with a question mark, however it is conventional in Ruby to do so.
30
+ # @see the full example in the <a href="index.html">README</a>.
31
+ def predicate(method_name, &block)
32
+ self.class.instance_eval do
33
+ define_method(method_name, &block)
34
+ helper_method(method_name)
35
+ end
36
+ end
37
+
38
+ # a synonym is similar to a predicate but returns values besides true and false.
39
+ alias_method :synonym, :predicate
22
40
  end
data/test/test_condi.rb CHANGED
@@ -7,6 +7,8 @@ require 'mocha'
7
7
  require 'action_controller'
8
8
  require 'condi'
9
9
 
10
+ require 'ostruct'
11
+
10
12
  #require 'ruby-debug'
11
13
 
12
14
 
@@ -84,8 +86,60 @@ class CondiTest < Test::Unit::TestCase
84
86
  assert_raise RuntimeError do
85
87
  puts "ah ha!" if @controller_instance.blows_up?
86
88
  end
87
-
88
89
  end
89
90
 
91
+ # predicates can be used with collections!
92
+ def test_collection_predicate
93
+ @controller_instance.instance_eval do
94
+ predicate(:is_usa?) do |location|
95
+ case location
96
+ when :arizona then true
97
+ when :massachusetts then true
98
+ else false
99
+ end
100
+ end
101
+ end
102
+
103
+ assert @controller_instance.respond_to?(:is_usa?)
104
+
105
+ @locations = [:arizona, :massachusetts, :england]
106
+ expected_values = [true, true, false]
107
+ @locations.each_with_index do |location,i|
108
+ assert @controller_instance.is_usa?(location) == expected_values[i]
109
+ end
110
+ end
111
+
112
+ # `synonym` is an alias for `predicate` for when you want to return something besides a boolean.
113
+ def test_synonyms
114
+ @controller_instance.instance_eval do
115
+ synonym(:css_for_item_status) do |item|
116
+ if item.status == :shipped
117
+ if item.delivery_status !~ /arrived/
118
+ "shipping"
119
+ else
120
+ "shipped"
121
+ end
122
+ else
123
+ "processing"
124
+ end
125
+ end
126
+ end
127
+
128
+ item = OpenStruct.new
129
+ item.status = :shipped
130
+ item.delivery_status = "order left Ohio. checkin MN. checkin AZ."
131
+ assert @controller_instance.css_for_item_status(item) == "shipping"
132
+
133
+ item = OpenStruct.new
134
+ item.status = :shipped
135
+ item.delivery_status = "order left Ohio. checkin MN. checkin AZ. arrived HI."
136
+ assert @controller_instance.css_for_item_status(item) == "shipped"
137
+
138
+ item = OpenStruct.new
139
+ item.status = :awaiting_payment
140
+ item.delivery_status = ""
141
+ assert @controller_instance.css_for_item_status(item) == "processing"
142
+ end
143
+
90
144
 
91
145
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: condi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -19,7 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - lib/condi.rb
21
21
  - README.md
22
- - LICENSE
22
+ - MIT-LICENSE
23
23
  - test/test_condi.rb
24
24
  homepage: http://github.com/coldnebo/condi
25
25
  licenses: []