active_type 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e159c5c5648aa7f407cd38c73d3e05fa630aa2a4
4
- data.tar.gz: 2bcb2536092006f2b519596e5f7b5c0655308920
3
+ metadata.gz: bf3e434ef181d6b90bc9254d3d00824e02abc575
4
+ data.tar.gz: 83883dad267701774625763bde13c919fb89f4f7
5
5
  SHA512:
6
- metadata.gz: 6d1f0bac7db1d6382c1ab3f62c6e1f72bd82ac02a848b516340871bfeec9302fc95b09588ee7c040b6f0e35d8014640aa683238a8b9e909df7deff18a39f5c44
7
- data.tar.gz: 3d1a06ab7508e69fea48fdcb5e9fa6aeed6ee3c42778d168fa23576ba0c2dbd67c09d4c04bd3b8cfda77131d250997b1f1016763b3b626e2f368eefe319b69bc
6
+ metadata.gz: 749cf455d62f3463b339b3964fb2acad5375cc9357939f2c93fa780045bc233c8e6db7457fb83ba5061f0f9ea78d975ae49b5eeef99135a2571ffa293ebfa9d8
7
+ data.tar.gz: f4f5718c15b260464619ed1e0cb9c33b6fed16aed6f1503bc7ef8282219cafb852f8d5d6d5d08b17f4c8ae67f057963b482ef0c9409c12600408411fcdd4e608
data/.gitignore CHANGED
@@ -3,3 +3,4 @@ pkg
3
3
  tags
4
4
  *.gem
5
5
  .idea
6
+ tmp
data/.travis.yml CHANGED
@@ -3,7 +3,7 @@ rvm:
3
3
  - "1.8.7"
4
4
  - "1.9.3"
5
5
  - "2.0.0"
6
- - "2.1.0"
6
+ - "2.1.2"
7
7
  gemfile:
8
8
  - gemfiles/Gemfile.3.2
9
9
  - gemfiles/Gemfile.4.0
data/README.md CHANGED
@@ -63,6 +63,7 @@ class SignIn < ActiveType::Object
63
63
  attribute :email, :string
64
64
  attribute :date_of_birth, :date
65
65
  attribute :accepted_terms, :boolean
66
+ attribute :account_type
66
67
 
67
68
  end
68
69
  ```
@@ -70,11 +71,13 @@ end
70
71
  These attributes can be assigned via constructor, mass-assignment, and are automatically typecast:
71
72
 
72
73
  ```ruby
73
- sign_in = SignIn.new(date_of_birth: "1980-01-01", accepted_terms: "1")
74
+ sign_in = SignIn.new(date_of_birth: "1980-01-01", accepted_terms: "1", account_type: AccountType::Trial.new)
74
75
  sign_in.date_of_birth.class # Date
75
76
  sign_in.accepted_terms? # true
76
77
  ```
77
78
 
79
+ ActiveType knows all the types that are allowed in migrations (i.e. `:string`, `:integer`, `:float`, `:decimal`, `:datetime`, `:time`, `:date`, `:boolean`). You can also skip the type to have a virtual attribute without typecasting.
80
+
78
81
  **`ActiveType::Object` actually inherits from `ActiveRecord::Base`, but simply skips all database access, inspired by [ActiveRecord Tableless](https://github.com/softace/activerecord-tableless).**
79
82
 
80
83
  This means your object has all usual `ActiveRecord::Base` methods. Some of those might not work properly, however. What does work:
@@ -87,7 +90,9 @@ This means your object has all usual `ActiveRecord::Base` methods. Some of those
87
90
 
88
91
  ### ActiveType::Record
89
92
 
90
- `ActiveType::Record` is simply `ActiveRecord::Base` plus the (virtual) attributes from `ActiveType::Object`. You can declare, assign, validate etc those attributes, but they will not be persisted.
93
+ If you have a database backed record (that inherits from `ActiveRecord::Base`), but also want to declare virtual attributes, simply inherit from `ActiveType::Record`.
94
+
95
+ Virtual attributes will not be persisted.
91
96
 
92
97
 
93
98
  ### ActiveType::Record[BaseClass]
@@ -102,6 +107,168 @@ class SignUp < ActiveType::Record[User]
102
107
  end
103
108
  ```
104
109
 
110
+ ### Inheriting from ActiveType:: objects
111
+
112
+ If you want to inherit from an ActiveType class, simply do
113
+
114
+ ```ruby
115
+ class SignUp < ActiveType::Record[User]
116
+ # ...
117
+ end
118
+
119
+ class SpecialSignUp < SignUp
120
+ # ...
121
+ end
122
+ ```
123
+
124
+ ### Defaults ####
125
+
126
+ Attributes can have defaults. Those are lazily evaluated on the first read, if no value has been set.
127
+
128
+ ```ruby
129
+ class SignIn < ActiveType::Object
130
+
131
+ attribute :created_at, :datetime, default: proc { Time.now }
132
+
133
+ end
134
+ ```
135
+
136
+ The proc is evaluated in the context of the object, so you can do
137
+
138
+ ```ruby
139
+ class SignIn < ActiveType::Object
140
+
141
+ attribute :email, :string
142
+ attribute :nickname, :string, default: proc { email.split('@').first }
143
+
144
+ end
145
+
146
+ SignIn.new(email: "tobias@example.org").nickname # "tobias"
147
+ SignIn.new(email: "tobias@example.org", :nickname => "kratob").nickname # "kratob"
148
+ ```
149
+
150
+ ### Nested attributes
151
+
152
+ ActiveType supports its own variant of nested attributes via the `nests_one` /
153
+ `nests_many` macros. The intention is to be mostly compatible with
154
+ `ActiveRecord`'s `accepts_nested_attributes` functionality.
155
+
156
+ Assume you have a list of records, say representing holidays, and you want to support bulk
157
+ editing. Then you could do something like:
158
+
159
+ ```ruby
160
+ class Holiday < ActiveRecord::Base
161
+ validates :date, presence: true
162
+ end
163
+
164
+ class HolidaysForm < ActiveType::Object
165
+ nests_many :holidays, reject_if: :all_blank, default: proc { Holiday.all }
166
+ end
167
+
168
+ class HolidaysController < ApplicationController
169
+ def edit
170
+ @holidays_form = HolidaysForm.new
171
+ end
172
+
173
+ def update
174
+ @holidays_form = HolidaysForm.new(params[:holidays_form])
175
+ if @holidays_form.save
176
+ redirect_to root_url, notice: "Success!"
177
+ else
178
+ render :edit
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+ # and in the view
185
+ <%= form_for @holidays_form, url: '/holidays', method: :put do |form| %>
186
+ <ul>
187
+ <%= form.fields_for :holidays do |holiday_form| %>
188
+ <li><%= holiday_form.text_field :date %></li>
189
+ <% end %>
190
+ </ul>
191
+ <% end %>
192
+ ```
193
+
194
+ - You have to say `nests_many :records`
195
+ - `records` will be validated and saved automatically
196
+ - The generated `.records_attributes =` expects parameters like `ActiveRecord`'s nested attributes, and words together with the `fields_for` helper:
197
+
198
+ - either as a hash (where the keys are meaningless)
199
+
200
+ ```ruby
201
+ {
202
+ '1' => { date: "new record's date" },
203
+ '2' => { id: '3', date: "existing record's date" }
204
+ }
205
+ ```
206
+
207
+ - or as an array
208
+
209
+ ```ruby
210
+ {
211
+ [ date: "new record's date" ],
212
+ [ id: '3', date: "existing record's date" ]
213
+ }
214
+ ```
215
+
216
+ To use it with single records, use `nests_one`. It works like `accept_nested_attributes` does for `has_one`.
217
+
218
+ Supported options for `nests_many` / `nests_one` are:
219
+ - `build_scope`
220
+
221
+ Used to build new records, for example:
222
+
223
+ ```ruby
224
+ nests_many :documents, build_scope: proc { Document.where(:state => "fresh") }
225
+ ```
226
+
227
+ - `find_scope`
228
+
229
+ Used to find existing records (in order to update them).
230
+
231
+ - `scope`
232
+
233
+ Sets `find_scope` and `build_scope` together.
234
+
235
+ If you don't supply a scope, `ActiveType` will guess from the association name, i.e. saying
236
+
237
+ ```ruby
238
+ nests_many :documents
239
+ ```
240
+
241
+ is the same as saying
242
+
243
+ ```ruby
244
+ nests_many :documents, scope: proc { Document }
245
+ ```
246
+
247
+ which is identical to
248
+
249
+ ```ruby
250
+ nests_many :documents, build_scope: proc { Document }, find_scope: proc { Document }
251
+ ```
252
+
253
+ All `...scope` options are evaled in the context of the record on first use, and cached.
254
+
255
+ - `allow_destroy`
256
+
257
+ Allow to destroy records if the attributes contain `_destroy => '1'`
258
+
259
+ - `reject_if`
260
+
261
+ Pass either a proc of the form `proc { |attributes| ... }`, or a symbol indicating a method, or `:all_blank`.
262
+
263
+ Will reject attributes for which the proc or the method returns true, or with only blank values (for `:all_blank`).
264
+
265
+ - `default`
266
+
267
+ Initializes the association on first access with the given proc:
268
+
269
+ ```ruby
270
+ nests_many :documents, default: proc { Documents.all }
271
+ ```
105
272
 
106
273
 
107
274
  Supported Rails versions
@@ -109,7 +276,12 @@ Supported Rails versions
109
276
 
110
277
  ActiveType is tested against ActiveRecord 3.2, 4.0 and 4.1.
111
278
 
112
- Later versions might work, earlier version will not.
279
+ Later versions might work, earlier will not.
280
+
281
+ Supported Ruby versions
282
+ ------------------------
283
+
284
+ ActiveType is tested against MRI 1.8.7 (for 3.2 only), 1.9.3, 2.0.0, 2.1.2.
113
285
 
114
286
 
115
287
  Installation
data/Rakefile CHANGED
@@ -11,8 +11,7 @@ namespace :all do
11
11
  task :spec do
12
12
  success = true
13
13
  for_each_gemfile do
14
- env = "SPEC=../../#{ENV['SPEC']} " if ENV['SPEC']
15
- success &= system("#{env} bundle exec rspec spec")
14
+ success &= system("bundle exec rspec spec")
16
15
  end
17
16
  fail "Tests failed" unless success
18
17
  end
data/gemfiles/Gemfile.3.2 CHANGED
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '~>3.2.0'
4
- gem 'rspec'
4
+ gem 'rspec', '<2.99'
5
5
  gem 'sqlite3'
6
6
 
7
7
  gem 'active_type', :path => '..'
@@ -1,28 +1,28 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_type (0.1.2)
4
+ active_type (0.2.0)
5
5
  activerecord (>= 3.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (3.2.17)
11
- activesupport (= 3.2.17)
10
+ activemodel (3.2.19)
11
+ activesupport (= 3.2.19)
12
12
  builder (~> 3.0.0)
13
- activerecord (3.2.17)
14
- activemodel (= 3.2.17)
15
- activesupport (= 3.2.17)
13
+ activerecord (3.2.19)
14
+ activemodel (= 3.2.19)
15
+ activesupport (= 3.2.19)
16
16
  arel (~> 3.0.2)
17
17
  tzinfo (~> 0.3.29)
18
- activesupport (3.2.17)
18
+ activesupport (3.2.19)
19
19
  i18n (~> 0.6, >= 0.6.4)
20
20
  multi_json (~> 1.0)
21
21
  arel (3.0.3)
22
22
  builder (3.0.4)
23
23
  diff-lcs (1.2.5)
24
- i18n (0.6.9)
25
- multi_json (1.9.2)
24
+ i18n (0.6.11)
25
+ multi_json (1.10.1)
26
26
  rspec (2.14.1)
27
27
  rspec-core (~> 2.14.0)
28
28
  rspec-expectations (~> 2.14.0)
@@ -32,7 +32,7 @@ GEM
32
32
  diff-lcs (>= 1.1.3, < 2.0)
33
33
  rspec-mocks (2.14.6)
34
34
  sqlite3 (1.3.9)
35
- tzinfo (0.3.39)
35
+ tzinfo (0.3.40)
36
36
 
37
37
  PLATFORMS
38
38
  ruby
@@ -40,5 +40,5 @@ PLATFORMS
40
40
  DEPENDENCIES
41
41
  active_type!
42
42
  activerecord (~> 3.2.0)
43
- rspec
43
+ rspec (< 2.99)
44
44
  sqlite3
data/gemfiles/Gemfile.4.0 CHANGED
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '~>4.0.0'
4
- gem 'rspec'
4
+ gem 'rspec', '<2.99'
5
5
  gem 'sqlite3'
6
6
 
7
7
  gem 'active_type', :path => '..'
@@ -1,34 +1,33 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_type (0.1.2)
4
+ active_type (0.2.0)
5
5
  activerecord (>= 3.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (4.0.4)
11
- activesupport (= 4.0.4)
10
+ activemodel (4.0.8)
11
+ activesupport (= 4.0.8)
12
12
  builder (~> 3.1.0)
13
- activerecord (4.0.4)
14
- activemodel (= 4.0.4)
13
+ activerecord (4.0.8)
14
+ activemodel (= 4.0.8)
15
15
  activerecord-deprecated_finders (~> 1.0.2)
16
- activesupport (= 4.0.4)
16
+ activesupport (= 4.0.8)
17
17
  arel (~> 4.0.0)
18
18
  activerecord-deprecated_finders (1.0.3)
19
- activesupport (4.0.4)
19
+ activesupport (4.0.8)
20
20
  i18n (~> 0.6, >= 0.6.9)
21
21
  minitest (~> 4.2)
22
22
  multi_json (~> 1.3)
23
23
  thread_safe (~> 0.1)
24
24
  tzinfo (~> 0.3.37)
25
25
  arel (4.0.2)
26
- atomic (1.1.16)
27
26
  builder (3.1.4)
28
27
  diff-lcs (1.2.5)
29
- i18n (0.6.9)
28
+ i18n (0.6.11)
30
29
  minitest (4.7.5)
31
- multi_json (1.9.2)
30
+ multi_json (1.10.1)
32
31
  rspec (2.14.1)
33
32
  rspec-core (~> 2.14.0)
34
33
  rspec-expectations (~> 2.14.0)
@@ -38,9 +37,8 @@ GEM
38
37
  diff-lcs (>= 1.1.3, < 2.0)
39
38
  rspec-mocks (2.14.6)
40
39
  sqlite3 (1.3.9)
41
- thread_safe (0.3.1)
42
- atomic (>= 1.1.7, < 2)
43
- tzinfo (0.3.39)
40
+ thread_safe (0.3.4)
41
+ tzinfo (0.3.40)
44
42
 
45
43
  PLATFORMS
46
44
  ruby
@@ -48,5 +46,5 @@ PLATFORMS
48
46
  DEPENDENCIES
49
47
  active_type!
50
48
  activerecord (~> 4.0.0)
51
- rspec
49
+ rspec (< 2.99)
52
50
  sqlite3
data/gemfiles/Gemfile.4.1 CHANGED
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'activerecord', '~>4.1.0.rc2'
4
- gem 'rspec'
3
+ gem 'activerecord', '~>4.1.0'
4
+ gem 'rspec', '<2.99'
5
5
  gem 'sqlite3'
6
6
 
7
7
  gem 'active_type', :path => '..'
@@ -1,32 +1,31 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_type (0.1.2)
4
+ active_type (0.2.0)
5
5
  activerecord (>= 3.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (4.1.0.rc2)
11
- activesupport (= 4.1.0.rc2)
10
+ activemodel (4.1.4)
11
+ activesupport (= 4.1.4)
12
12
  builder (~> 3.1)
13
- activerecord (4.1.0.rc2)
14
- activemodel (= 4.1.0.rc2)
15
- activesupport (= 4.1.0.rc2)
13
+ activerecord (4.1.4)
14
+ activemodel (= 4.1.4)
15
+ activesupport (= 4.1.4)
16
16
  arel (~> 5.0.0)
17
- activesupport (4.1.0.rc2)
17
+ activesupport (4.1.4)
18
18
  i18n (~> 0.6, >= 0.6.9)
19
19
  json (~> 1.7, >= 1.7.7)
20
20
  minitest (~> 5.1)
21
21
  thread_safe (~> 0.1)
22
22
  tzinfo (~> 1.1)
23
- arel (5.0.0)
24
- atomic (1.1.16)
23
+ arel (5.0.1.20140414130214)
25
24
  builder (3.2.2)
26
25
  diff-lcs (1.2.5)
27
- i18n (0.6.9)
26
+ i18n (0.6.11)
28
27
  json (1.8.1)
29
- minitest (5.3.1)
28
+ minitest (5.4.0)
30
29
  rspec (2.14.1)
31
30
  rspec-core (~> 2.14.0)
32
31
  rspec-expectations (~> 2.14.0)
@@ -36,9 +35,8 @@ GEM
36
35
  diff-lcs (>= 1.1.3, < 2.0)
37
36
  rspec-mocks (2.14.6)
38
37
  sqlite3 (1.3.9)
39
- thread_safe (0.3.1)
40
- atomic (>= 1.1.7, < 2)
41
- tzinfo (1.1.0)
38
+ thread_safe (0.3.4)
39
+ tzinfo (1.2.1)
42
40
  thread_safe (~> 0.1)
43
41
 
44
42
  PLATFORMS
@@ -46,6 +44,6 @@ PLATFORMS
46
44
 
47
45
  DEPENDENCIES
48
46
  active_type!
49
- activerecord (~> 4.1.0.rc2)
50
- rspec
47
+ activerecord (~> 4.1.0)
48
+ rspec (< 2.99)
51
49
  sqlite3
@@ -0,0 +1,128 @@
1
+ require 'active_record/errors'
2
+
3
+ module ActiveType
4
+
5
+ module NestedAttributes
6
+
7
+ class RecordNotFound < ActiveRecord::RecordNotFound; end
8
+
9
+ class Association
10
+
11
+ def initialize(owner, target_name, options = {})
12
+ options.assert_valid_keys(:build_scope, :find_scope, :scope, :allow_destroy, :reject_if)
13
+
14
+ @owner = owner
15
+ @target_name = target_name.to_sym
16
+ @allow_destroy = options.fetch(:allow_destroy, false)
17
+ @reject_if = options.delete(:reject_if)
18
+ @options = options.dup
19
+ end
20
+
21
+ def assign_attributes(parent, attributes)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def save(parent)
26
+ keep = assigned_children(parent)
27
+ changed_children(parent).each do |child|
28
+ if child.marked_for_destruction?
29
+ child.destroy if child.persisted?
30
+ keep.delete(child)
31
+ else
32
+ child.save(:validate => false) or raise ActiveRecord::Rollback
33
+ end
34
+ end
35
+ assign_children(parent, keep)
36
+ end
37
+
38
+ def validate(parent)
39
+ changed_children(parent).each do |child|
40
+ unless child.valid?
41
+ child.errors.each do |attribute, message|
42
+ attribute = "#{@target_name}.#{attribute}"
43
+ parent.errors[attribute] << message
44
+ parent.errors[attribute].uniq!
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def add_child(parent, child_or_children)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def assigned_children(parent)
57
+ Array.wrap(parent[@target_name])
58
+ end
59
+
60
+ def assign_children(parent, children)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ def changed_children(parent)
65
+ assigned_children(parent).select(&:changed_for_autosave?)
66
+ end
67
+
68
+ def build_child(parent, attributes)
69
+ build_scope(parent).new(attributes)
70
+ end
71
+
72
+ def scope(parent)
73
+ scope_for(parent, :scope) || derive_class_name.constantize
74
+ end
75
+
76
+ def build_scope(parent)
77
+ scope_for(parent, :build_scope) || scope(parent)
78
+ end
79
+
80
+ def find_scope(parent)
81
+ scope_for(parent, :find_scope) || scope(parent)
82
+ end
83
+
84
+ def scope_for(parent, key)
85
+ parent._nested_attribute_scopes ||= {}
86
+ parent._nested_attribute_scopes[[self, key]] ||= begin
87
+ scope = @options[key]
88
+ scope.respond_to?(:call) ? parent.instance_eval(&scope) : scope
89
+ end
90
+ end
91
+
92
+ def derive_class_name
93
+ raise NotImplementedError
94
+ end
95
+
96
+ def fetch_child(parent, id)
97
+ assigned = assigned_children(parent).detect { |r| r.id == id }
98
+ return assigned if assigned
99
+
100
+ if child = find_scope(parent).find_by_id(id)
101
+ add_child(parent, child)
102
+ child
103
+ else
104
+ raise RecordNotFound, "could not find a child record with id '#{id}' for '#{@target_name}'"
105
+ end
106
+ end
107
+
108
+ def truthy?(value)
109
+ ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
110
+ end
111
+
112
+ def reject?(parent, attributes)
113
+ result = case @reject_if
114
+ when :all_blank
115
+ attributes.all? { |key, value| key == '_destroy' || value.blank? }
116
+ when Proc
117
+ @reject_if.call(attributes)
118
+ when Symbol
119
+ parent.method(@reject_if).arity == 0 ? parent.send(@reject_if) : parent.send(@reject_if, attributes)
120
+ end
121
+ result
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,67 @@
1
+ require 'active_type/nested_attributes/nests_one_association'
2
+ require 'active_type/nested_attributes/nests_many_association'
3
+
4
+ module ActiveType
5
+
6
+ module NestedAttributes
7
+
8
+ class Builder
9
+
10
+ def initialize(owner, mod)
11
+ @owner = owner
12
+ @module = mod
13
+ end
14
+
15
+ def build(name, one_or_many, options)
16
+ add_attribute(name, options.slice(:default))
17
+ association = build_association(name, one_or_many == :one, options.except(:default))
18
+ add_writer_method(name, association)
19
+ add_autosave(name, association)
20
+ add_validation(name, association)
21
+ end
22
+
23
+
24
+ private
25
+
26
+ def build_association(name, singular, options)
27
+ (singular ? NestsOneAssociation : NestsManyAssociation).new(@owner, name, options)
28
+ end
29
+
30
+ def add_attribute(name, options)
31
+ @owner.attribute(name, options)
32
+ end
33
+
34
+ def add_writer_method(name, association)
35
+ write_method = "#{name}_attributes="
36
+ @module.module_eval do
37
+ define_method write_method do |attributes|
38
+ association.assign_attributes(self, attributes)
39
+ end
40
+ end
41
+ end
42
+
43
+ def add_autosave(name, association)
44
+ save_method = "save_associated_records_for_#{name}"
45
+ @module.module_eval do
46
+ define_method save_method do
47
+ association.save(self)
48
+ end
49
+ end
50
+ @owner.after_save save_method
51
+ end
52
+
53
+ def add_validation(name, association)
54
+ validate_method = "validate_associated_records_for_#{name}"
55
+ @module.module_eval do
56
+ define_method validate_method do
57
+ association.validate(self)
58
+ end
59
+ end
60
+ @owner.validate validate_method
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end