formality 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/formality.rb +284 -0
- metadata +11 -10
data/lib/formality.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "active_model"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/hash_with_indifferent_access"
|
6
|
+
|
7
|
+
module Formality
|
8
|
+
VERSION = "0.0.2"
|
9
|
+
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
#
|
13
|
+
# ActiveModel Compliance
|
14
|
+
# ======================
|
15
|
+
#
|
16
|
+
|
17
|
+
# Gives classes including Formality the standard validations
|
18
|
+
# framework plus the :valid? and :invalid? methods
|
19
|
+
include ActiveModel::Validations
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
# :model_name must be defined on the class and return a
|
23
|
+
# String with various convenience methods. ActiveModel::Name
|
24
|
+
# gives us that.
|
25
|
+
#
|
26
|
+
# By default, :model_name uses the name of the Form class.
|
27
|
+
def model_name
|
28
|
+
@__model_name ||= ActiveModel::Name.new(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# More ActiveModel compliance shenanigans.
|
33
|
+
def to_key; end
|
34
|
+
def to_param; end
|
35
|
+
def to_partial_path; "" end
|
36
|
+
|
37
|
+
# When Formality is included into a class, it defines an
|
38
|
+
# attr_accessor for :id. This in combination with the
|
39
|
+
# definition of :persisted? helps Formality forms work
|
40
|
+
# cleanly with :form_for.
|
41
|
+
included do
|
42
|
+
attr_accessor :id
|
43
|
+
end
|
44
|
+
|
45
|
+
# :form_for calls :persisted? on the object it receives
|
46
|
+
# to determine whether to :post or :put.
|
47
|
+
#
|
48
|
+
# We assume we're persisted (i.e. editing an object) if
|
49
|
+
# we have an id.
|
50
|
+
def persisted?
|
51
|
+
id.present?
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Attributes
|
56
|
+
# ==========
|
57
|
+
#
|
58
|
+
|
59
|
+
module ClassMethods
|
60
|
+
# Declare an attribute.
|
61
|
+
#
|
62
|
+
# Defines a reader and writer. Accepts a :default options
|
63
|
+
# for the default value of the attribute.
|
64
|
+
def attribute(name, options={})
|
65
|
+
attributes << name.to_s
|
66
|
+
define_reader(name, options[:default])
|
67
|
+
attr_writer name
|
68
|
+
end
|
69
|
+
|
70
|
+
# A Set of attribute names, stored on the form class.
|
71
|
+
def attributes
|
72
|
+
@__attributes ||= Set.new
|
73
|
+
end
|
74
|
+
|
75
|
+
# A convenience class method for creating and assigning
|
76
|
+
# a Hash to a form.
|
77
|
+
def assign(attrs)
|
78
|
+
new.assign(attrs)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Defines an attribute reader with an
|
84
|
+
# optional default value.
|
85
|
+
def define_reader(name, default=nil)
|
86
|
+
class_eval <<-reader
|
87
|
+
def #{name}
|
88
|
+
@#{name} ||= #{default.inspect}
|
89
|
+
end
|
90
|
+
reader
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns an Array of attribute names (Strings).
|
95
|
+
def attribute_names
|
96
|
+
self.class.attributes.to_a
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a HashWithIndifferentAccess of all the
|
100
|
+
# defined attributes and their values.
|
101
|
+
def attributes
|
102
|
+
hash = ActiveSupport::HashWithIndifferentAccess.new
|
103
|
+
attribute_names.each_with_object({}) do |name|
|
104
|
+
hash[name] = send(name)
|
105
|
+
end
|
106
|
+
hash
|
107
|
+
end
|
108
|
+
|
109
|
+
# Assigns a hash of attributes to the form. Only assigns
|
110
|
+
# values if the key for that value is a declared
|
111
|
+
# attribute. It silently ignores non-declared keys.
|
112
|
+
def assign(new_attributes)
|
113
|
+
new_attributes.each do |name, value|
|
114
|
+
next unless attribute?(name)
|
115
|
+
send("#{name}=", value)
|
116
|
+
end
|
117
|
+
self.id = new_attributes[:id]
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns a Boolean that answers the question: Is this `name`
|
122
|
+
# a declared attribute?
|
123
|
+
def attribute?(name)
|
124
|
+
attribute_names.include?(name.to_s)
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Working with models
|
129
|
+
# ===================
|
130
|
+
#
|
131
|
+
|
132
|
+
module ClassMethods
|
133
|
+
# Declare the model that this form object represents.
|
134
|
+
#
|
135
|
+
# Purely a convenience so that you don't have to
|
136
|
+
# specify the :url parameter in your :form_for calls.
|
137
|
+
def model(name_sym)
|
138
|
+
model_klass = name_sym.to_s.capitalize.constantize
|
139
|
+
@__model_name = ActiveModel::Name.new(model_klass)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Build a form object from an existing model.
|
143
|
+
#
|
144
|
+
# If nested forms were declared with the
|
145
|
+
# :from_model_attribute option, it will also
|
146
|
+
# build the nested form object(s).
|
147
|
+
def from_model(model)
|
148
|
+
new.tap do |form|
|
149
|
+
form.id = model.id
|
150
|
+
form.assign(model.attributes)
|
151
|
+
nested_forms.each do |nested|
|
152
|
+
form.send("#{nested}=", model)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Some Controller Sugar
|
160
|
+
# =====================
|
161
|
+
#
|
162
|
+
# class FooController < ApplicationController
|
163
|
+
# def create
|
164
|
+
# form = FooForm.new.assign(params[:foo_form])
|
165
|
+
#
|
166
|
+
# form.valid do
|
167
|
+
# current_user.foos.create(@form.attributes)
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# form.invalid do
|
171
|
+
# @form = form
|
172
|
+
# render :new
|
173
|
+
# end
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
|
178
|
+
# Yields to its block if the form is valid.
|
179
|
+
def valid; yield if valid? end
|
180
|
+
|
181
|
+
# Same as :valid, but in reverse: only yields to
|
182
|
+
# the block if the form is invalid.
|
183
|
+
def invalid; yield if invalid? end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Nesting
|
187
|
+
# =======
|
188
|
+
#
|
189
|
+
# Allows forms to have forms nested within them that
|
190
|
+
# work nicely with Rails' :fields_for method.
|
191
|
+
#
|
192
|
+
# Validations are called on nested forms, so that if
|
193
|
+
# any nested form is invalid, so is the parent.
|
194
|
+
#
|
195
|
+
|
196
|
+
module ClassMethods
|
197
|
+
# Singular Nesting.
|
198
|
+
def nest_one(child, options={})
|
199
|
+
add_nested_form(child)
|
200
|
+
define_nested_form_one(child, options)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Plural nesting.
|
204
|
+
#
|
205
|
+
# Works just like :nest_one, except it works for
|
206
|
+
# an Array of nested forms.
|
207
|
+
def nest_many(children, options={})
|
208
|
+
add_nested_form(children)
|
209
|
+
define_nested_form_many(children, options)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Keep track of what forms we've nested.
|
213
|
+
def nested_forms
|
214
|
+
@__nested_forms ||= Set.new
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def add_nested_form(nested)
|
220
|
+
attributes << "#{nested}_attributes"
|
221
|
+
nested_forms << nested
|
222
|
+
end
|
223
|
+
|
224
|
+
# Define the accessors for a singular nested form.
|
225
|
+
def define_nested_form_one(name, options={})
|
226
|
+
define_reader(name)
|
227
|
+
from_model_attribute = options[:from_model_attribute]
|
228
|
+
class_eval <<-one
|
229
|
+
def #{name}_attributes
|
230
|
+
self.#{name} ? @#{name}.attributes : nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def #{name}_attributes=(attrs)
|
234
|
+
form_klass = "#{name}".classify.constantize
|
235
|
+
@#{name} = form_klass.new.assign(attrs)
|
236
|
+
end
|
237
|
+
|
238
|
+
def #{name}=(model)
|
239
|
+
return unless #{from_model_attribute.inspect}
|
240
|
+
nested_model = model.send(#{from_model_attribute.inspect})
|
241
|
+
self.#{name}_attributes = nested_model.attributes
|
242
|
+
end
|
243
|
+
one
|
244
|
+
end
|
245
|
+
|
246
|
+
# Define the accessors for a plural nested form.
|
247
|
+
def define_nested_form_many(name, options={})
|
248
|
+
define_reader(name, [])
|
249
|
+
from_model_attribute = options[:from_model_attribute]
|
250
|
+
class_eval <<-many
|
251
|
+
def #{name}_attributes
|
252
|
+
self.#{name}.map { |form| form.attributes }
|
253
|
+
end
|
254
|
+
|
255
|
+
def #{name}_attributes=(attrs_array)
|
256
|
+
form_klass = "#{name}".classify.constantize
|
257
|
+
@#{name} = attrs_array.map do |attrs|
|
258
|
+
form_klass.new.assign(attrs)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def #{name}=(model)
|
263
|
+
return unless #{from_model_attribute.inspect}
|
264
|
+
nested_models = model.send(#{from_model_attribute.inspect})
|
265
|
+
self.#{name}_attributes = nested_models.map { |m| m.attributes }
|
266
|
+
end
|
267
|
+
many
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# A Formality form object is valid if its attributes
|
272
|
+
# validate and all of its children are valid.
|
273
|
+
def valid?(context=nil)
|
274
|
+
nested_forms_valid?(context) && super(context)
|
275
|
+
end
|
276
|
+
|
277
|
+
# If there are nested forms, call :valid? on them.
|
278
|
+
def nested_forms_valid?(context)
|
279
|
+
self.class.nested_forms.all? do |name|
|
280
|
+
nested = Array(send(name))
|
281
|
+
nested.all? { |form| form.valid?(context) }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: formality
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activemodel
|
16
|
-
requirement: &
|
16
|
+
requirement: &70323137369080 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.0.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70323137369080
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activesupport
|
27
|
-
requirement: &
|
27
|
+
requirement: &70323137368580 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 3.0.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70323137368580
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: actionpack
|
38
|
-
requirement: &
|
38
|
+
requirement: &70323137368120 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 3.0.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70323137368120
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: tst
|
49
|
-
requirement: &
|
49
|
+
requirement: &70323137367740 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70323137367740
|
58
58
|
description: ! 'ActiveModel-compliant form objects for rails app.
|
59
59
|
|
60
60
|
|
@@ -74,6 +74,7 @@ files:
|
|
74
74
|
- LICENSE
|
75
75
|
- Rakefile
|
76
76
|
- README.md
|
77
|
+
- lib/formality.rb
|
77
78
|
- test/attributes.rb
|
78
79
|
- test/compliance.rb
|
79
80
|
- test/lint.rb
|