condi 0.0.1 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/{LICENSE → MIT-LICENSE} +1 -1
- data/README.md +113 -10
- data/lib/condi.rb +32 -14
- data/test/test_condi.rb +55 -1
- metadata +2 -2
data/{LICENSE → MIT-LICENSE}
RENAMED
@@ -1,7 +1,7 @@
|
|
1
1
|
MIT LICENSE
|
2
2
|
from http://www.opensource.org/licenses/mit-license.php
|
3
3
|
|
4
|
-
Copyright (C)
|
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)
|
14
|
+
Copyright (C) 2012 by Larry Kyrala. MIT Licensed.
|
15
|
+
|
16
|
+
Installation
|
17
|
+
------------
|
12
18
|
|
13
|
-
|
14
|
-
-------
|
19
|
+
gem install condi
|
15
20
|
|
16
|
-
|
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,
|
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
|
-
|
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
|
-
*
|
224
|
+
* Rules-engines exist (i.e. `rools`), but are heavier-weight than Condi and can be complex to use.
|
125
225
|
|
126
|
-
*
|
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.
|
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
|
-
|
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
|
-
#
|
12
|
-
# @
|
13
|
-
#
|
14
|
-
# @
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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.
|
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: []
|