condi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +130 -0
  3. data/lib/condi.rb +22 -0
  4. data/test/test_condi.rb +91 -0
  5. metadata +50 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT LICENSE
2
+ from http://www.opensource.org/licenses/mit-license.php
3
+
4
+ Copyright (C) 2011 by Larry Kyrala
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+
2
+ Condi
3
+ =====
4
+
5
+ Condi is a gem that you use with Rails to make it easier to cleanly implement conditional elements in a view.
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.
8
+
9
+ API Doc: {Condi}
10
+
11
+ Copyright (C) 2011 by Larry Kyrala. MIT Licensed.
12
+
13
+ Example
14
+ -------
15
+
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:
17
+
18
+ `app/controllers/store_controller.rb:`
19
+
20
+ class StoreController
21
+ include Condi
22
+
23
+ def checkout
24
+ user = User.find(user_id)
25
+ cart = Cart.find(cart_id)
26
+ predicate(:show_free_shipping?) { user.new_customer? && cart.amount > 100 }
27
+ end
28
+ end
29
+
30
+ `app/views/store/checkout.html.erb:`
31
+
32
+ <% form_for @cart do |f| %>
33
+ <% if show_free_shipping? %>
34
+ <%= f.radio_button("cart", "shipping", "free_ground") %>
35
+ <% end %>
36
+ <% end %>
37
+
38
+ The Problem
39
+ -----------
40
+
41
+ Sometimes, pieces of your UI need to be enabled or disabled depending on certain criteria. Usually these criteria come from Models loaded during actions in your Controllers.
42
+
43
+ Here's a typical implementation of the above example without Condi:
44
+
45
+ class StoreController
46
+ def checkout
47
+ @user = User.find(user_id)
48
+ @cart = Cart.find(cart_id)
49
+ end
50
+ end
51
+
52
+ <% form_for @cart do |f| %>
53
+ <% if @user.new_customer? && @cart.amount > 100 %>
54
+ <%= f.radio_button("cart", "shipping", "free_ground") %>
55
+ <% end %>
56
+ <% end %>
57
+
58
+ Not the cleanest approach since business logic is in our views now. What's another alternative? Maybe we can stick a predicate for displaying ground shipping on the Cart model instead?
59
+
60
+ class Cart
61
+ def show_free_shipping?(new_customer)
62
+ new_customer && amount > 100
63
+ end
64
+ end
65
+
66
+ <% form_for @cart do |f| %>
67
+ <% if @cart.show_free_shipping?(@user.new_customer?) %>
68
+ <%= f.radio_button("cart", "shipping", "free_ground") %>
69
+ <% end %>
70
+ <% end %>
71
+
72
+ We haven't gained much except shuffling arguments around.
73
+
74
+
75
+ Or, we could put the predicate in a helper and remove the args:
76
+
77
+ class StoreHelper
78
+ def show_free_shipping?
79
+ @user.new_customer? && @cart.amount > 100
80
+ end
81
+ end
82
+
83
+ <% form_for @cart do |f| %>
84
+ <% if show_free_shipping? %>
85
+ <%= f.radio_button("cart", "shipping", "free_ground") %>
86
+ <% end %>
87
+ <% end %>
88
+
89
+ This is a little better, but now we have variables set up in the controller and business logic in the helper. It would be nicer if we could define the predicate in the controller where it is used, in the context of what the action has loaded (either into instance variables or locals). Also if we have a large data-driven UI, we may have many such conditional UI predicates. Managing them all can become quite complex.
90
+
91
+
92
+ Solution
93
+ --------
94
+
95
+ The way Condi solves this problem is to allow you to define predicates in the controller that are accessible in the view.
96
+
97
+ class StoreController
98
+ include Condi
99
+
100
+ def checkout
101
+ user = User.find(user_id)
102
+ cart = Cart.find(cart_id)
103
+ predicate(:show_free_shipping?) { user.new_customer? && cart.amount > 100 }
104
+ end
105
+ end
106
+
107
+ The `predicate` call creates a closure around the state we've loaded in the controller and makes it available to the view later without cluttering up the helper namespace or forcing the view to contain business logic.
108
+
109
+ <% form_for @cart do |f| %>
110
+ <% if show_free_shipping? %>
111
+ <%= f.radio_button("cart", "shipping", "free_ground") %>
112
+ <% end %>
113
+ <% end %>
114
+
115
+ How does this work? Behind the scenes, `predicate` defines an instance method on the controller and then marks it as a `helper_method` which allows Rails to call the predicate from the view. Since the predicate is dynamically added, you don't have to worry about the controller instance containing any more predicates than you defined in that particular action, so it's easy to manage.
116
+
117
+ Advantages
118
+ ----------
119
+
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.
121
+
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.
123
+
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.
125
+
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.
127
+
128
+
129
+
130
+
data/lib/condi.rb ADDED
@@ -0,0 +1,22 @@
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:
3
+ # class StoreController
4
+ # include Condi
5
+ # ...
6
+ # end
7
+ module Condi
8
+
9
+ # define a predicate (instance method) on the controller which is callable from the related view.
10
+ # @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
22
+ end
@@ -0,0 +1,91 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'test/unit'
5
+ require 'mocha'
6
+
7
+ require 'action_controller'
8
+ require 'condi'
9
+
10
+ #require 'ruby-debug'
11
+
12
+
13
+ class CondiTest < Test::Unit::TestCase
14
+
15
+ def setup
16
+ @controller_class = Class.new(::ActionController::Base)
17
+ @controller_class.instance_eval do
18
+ include Condi
19
+ end
20
+ @controller_instance = @controller_class.new
21
+ end
22
+
23
+ def test_included_properly
24
+ assert @controller_instance.respond_to?(:predicate)
25
+ end
26
+
27
+ def test_simple_predicate
28
+ @controller_instance.instance_eval do
29
+ predicate(:always_true?) { true }
30
+ end
31
+
32
+ assert @controller_instance.respond_to?(:always_true?)
33
+ assert @controller_instance.always_true? == true
34
+ end
35
+
36
+ def test_instance_variable_predicate
37
+ @controller_instance.instance_eval do
38
+ @var = 5
39
+ predicate(:is_var_5?) { @var == 5 }
40
+ end
41
+
42
+ assert @controller_instance.respond_to?(:is_var_5?)
43
+ assert @controller_instance.is_var_5? == true
44
+ end
45
+
46
+ def test_local_variable_predicate
47
+ @controller_instance.instance_eval do
48
+ var = "Mary"
49
+ predicate(:is_mary?) { (var =~ /Mary/) == 0 }
50
+ end
51
+
52
+ assert @controller_instance.respond_to?(:is_mary?)
53
+ assert @controller_instance.is_mary? == true
54
+ end
55
+
56
+ def test_complex_multiline_predicate
57
+ @controller_instance.instance_eval do
58
+ # simulating loaded AR objects in a controller method.
59
+ @customer = Object.new
60
+ def @customer.new_customer?
61
+ true
62
+ end
63
+ @cart = Object.new
64
+ def @cart.amount
65
+ 105
66
+ end
67
+ predicate(:show_free_shipping?) do
68
+ @customer.new_customer? && @cart.amount > 100
69
+ end
70
+ end
71
+
72
+ assert @controller_instance.respond_to?(:show_free_shipping?)
73
+ assert @controller_instance.show_free_shipping? == true
74
+ end
75
+
76
+ def test_exception_within_predicate
77
+ @controller_instance.instance_eval do
78
+ predicate(:blows_up?) do
79
+ raise "blew up!"
80
+ end
81
+ end
82
+
83
+ assert @controller_instance.respond_to?(:blows_up?)
84
+ assert_raise RuntimeError do
85
+ puts "ah ha!" if @controller_instance.blows_up?
86
+ end
87
+
88
+ end
89
+
90
+
91
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: condi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Larry Kyrala
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-14 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Conditional UI predicates for Rails
15
+ email: larry.kyrala@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/condi.rb
21
+ - README.md
22
+ - LICENSE
23
+ - test/test_condi.rb
24
+ homepage: http://github.com/coldnebo/condi
25
+ licenses: []
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements:
43
+ - rails
44
+ rubyforge_project:
45
+ rubygems_version: 1.8.10
46
+ signing_key:
47
+ specification_version: 3
48
+ summary: Condi
49
+ test_files:
50
+ - test/test_condi.rb