formality 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|