active_type 0.1.3 → 0.2.0

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.
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