model_view 0.2.0 → 0.3.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: 5bbb9e6927b9d2a9a2350d4b2b9fa3bb914a125d
4
- data.tar.gz: 58d8e67a612acda3b04469af9573ef2f71bfc2bb
3
+ metadata.gz: 7c5dbabbb8d69d5dadabc4592e8ac4dc6c705c26
4
+ data.tar.gz: 226889168c0093984f854e7717e5decf9658c19f
5
5
  SHA512:
6
- metadata.gz: 68014a9a7f17ad08e169fed2cb92d2582e886821e51bb7690412cec03263a97204bd1c80f127855f907ef52050abbbf42c5edf0b52e57fe64f99d3ab81233250
7
- data.tar.gz: 4283cf11f8300868265c6763fef87a6626d0cd61926bfa822fc7b23099848ba4e12d68f60fd8f59d174e05657cec2b2497657090d714aa75373429ccdb13da22
6
+ metadata.gz: 526855a236a1e2f026da812c2deca02ef02b174f7e0d37f44da02a4d5783ebc98c013c04efe2909b5cf3b451d6dafd01fa50b0f6c6a617852296bd2d0eaea0db
7
+ data.tar.gz: 1cdb32908ed57177c8f278d7af4730549a5b57fe5ad30555e32bcbf42c3e5faeb8eb3711a9f20663b2134d9062cac0a31a4e912d60270da698801fb8d2af8bbe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ # 0.3.0
4
+ * ModelView can be used to update models
5
+
6
+
3
7
  # 0.2.0
4
8
  * ModelView can add a convenience method to the model class
5
9
 
data/README.md CHANGED
@@ -33,7 +33,7 @@ field :name
33
33
 
34
34
  field(:first_name) { |person| person.name.split(' ').first }
35
35
 
36
- field :is_current_user, context[:current_user] do |person, current_user|
36
+ field :is_current_user, {context: current_user} do |person, current_user|
37
37
  person == current_user
38
38
  end
39
39
  ```
@@ -130,3 +130,40 @@ PersonView.as_hash(person, {context: {current_user: current_user}, scope: :all})
130
130
  is_current_user: true
131
131
  }
132
132
  ```
133
+
134
+ #### Updating models
135
+ `ModelView` can also be used to update models. This is achieved by creating setters.
136
+
137
+ The easiest way to create a setter is to set the `setter` flag to true when defining a field:
138
+ ```ruby
139
+ field :name, setter: true
140
+ ```
141
+
142
+ Setters can also be defined outside of the `field` macro:
143
+ ```ruby
144
+ setter :name
145
+ ```
146
+
147
+ Setters defined in this way are naive (`obj.send("#{field}=", value)`) and will not automatically call the `save` method on the model. See below on how to create an `after_update` hook.
148
+
149
+ Finally, for more complex updaters, a block can be passed:
150
+ ```ruby
151
+ setter(:name) do |obj, name|
152
+ first_name, last_name = name.split(" ")
153
+ obj.first_name = first_name
154
+ obj.last_name = last_name
155
+ obj.save!
156
+ end
157
+ ```
158
+
159
+ To avoid having to explicitly save the model in every block, one can define an `after_save` block:
160
+ ```ruby
161
+ after_save { |obj| obj.save! }
162
+
163
+ field :telephone_number, setter: true
164
+ setter(:name) do |obj, name|
165
+ first_name, last_name = name.split(" ")
166
+ obj.first_name = first_name
167
+ obj.last_name = last_name
168
+ end
169
+ ```
@@ -0,0 +1,76 @@
1
+ module ModelView
2
+ class Updater
3
+
4
+ class << self
5
+ def update(obj, scopes, data, scope=nil)
6
+ scope ||= ModelView::ROOT
7
+
8
+ setters = setters_for_scope(scope, scopes)
9
+
10
+ data.each do |key, value|
11
+ setter = if !setters[key].nil?
12
+ setter_for_key(setters, key)
13
+ else
14
+ nil
15
+ end
16
+
17
+ setter.call(obj, value) if setter
18
+ end
19
+
20
+ after_update = after_update_for_scope(scope, scopes)
21
+ after_update.call(obj) if after_update
22
+ end
23
+
24
+ private
25
+ def setter_for_key(setters, key)
26
+ return nil if setters[key].nil?
27
+ if setters[key][:block]
28
+ setters[key][:block]
29
+ else
30
+ lambda { |obj, value| obj.send("#{key}=", value) }
31
+ end
32
+ end
33
+
34
+ def after_update_for_scope(scope, scopes)
35
+ extended_scopes = scopes[scope][:extends] || []
36
+ first_extended_after_update = extended_scopes.reduce(nil) do |res, s|
37
+ res || extract_after_update(s, scopes)
38
+ end
39
+
40
+ extract_after_update(scope, scopes) ||
41
+ first_extended_after_update ||
42
+ extract_after_update(ModelView::ROOT, scopes)
43
+
44
+ end
45
+
46
+ def setters_for_scope(scope, scope_data)
47
+ root_scope_setters = extract_setters(ModelView::ROOT, scope_data)
48
+ scope_setters = scope == ModelView::ROOT ? {} : extract_setters(scope, scope_data)
49
+
50
+ extended_scopes = scope_data[scope][:extends] || []
51
+ extended_setters = extended_scopes.reduce({}) do |res, scope|
52
+ res.merge(extract_setters(scope, scope_data))
53
+ end
54
+
55
+ included_setters = (scope_data[scope][:includes] || []).reduce({}) do |res, scope_name|
56
+ res[scope_name] = setters_for_scope(scope_name, scope_data)
57
+ res
58
+ end
59
+
60
+ {}.merge(root_scope_setters)
61
+ .merge(scope_setters)
62
+ .merge(extended_setters)
63
+ .merge(included_setters)
64
+ end
65
+
66
+ def extract_setters(scope, scope_data)
67
+ scope_data[scope][:setters]
68
+ end
69
+
70
+ def extract_after_update(scope, scope_data)
71
+ scope_data[scope][:after_update]
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -1,3 +1,3 @@
1
1
  module ModelView
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/model_view.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'model_view/resolver'
2
+ require 'model_view/updater'
2
3
 
3
4
  module ModelView
4
5
 
@@ -22,6 +23,11 @@ module ModelView
22
23
  @current_scope = nil
23
24
  end
24
25
 
26
+ def setter(field_name, arg={}, &block)
27
+ scope_name = @current_scope || ROOT
28
+ add_setter scope_name, field_name, arg, block
29
+ end
30
+
25
31
  def include_scope(*scope)
26
32
  raise Exception.new("Root scope can not include another scope") if @current_scope.nil? || @current_scope == ROOT
27
33
  scope.flatten.each { |s| @scopes[@current_scope][:includes] << s }
@@ -32,6 +38,11 @@ module ModelView
32
38
  scope.flatten.each { |s| @scopes[@current_scope][:extends] << s }
33
39
  end
34
40
 
41
+ def after_update(&block)
42
+ scope_name = @current_scope || ROOT
43
+ add_after_update scope_name, block
44
+ end
45
+
35
46
  def scopes
36
47
  @scopes
37
48
  end
@@ -42,6 +53,11 @@ module ModelView
42
53
  ModelView::Resolver.resolve(object, @scopes, scope , context)
43
54
  end
44
55
 
56
+ def update(object, data, opts={})
57
+ scope = opts[:scope] || ROOT
58
+ ModelView::Updater.update(object, @scopes, data , scope)
59
+ end
60
+
45
61
  def model(model_class)
46
62
  model_view_class = self
47
63
  model_class.class_eval do
@@ -53,15 +69,34 @@ module ModelView
53
69
 
54
70
  private
55
71
 
72
+ def new_opts
73
+ {fields: {}, extends: [], includes: [], setters: {}, after_update: nil}
74
+ end
75
+
76
+ def add_after_update(scope, block)
77
+ @scopes ||= {ROOT => new_opts}
78
+ @scopes[scope] ||= new_opts
79
+ @scopes[scope][:after_update] = block
80
+ end
81
+
56
82
  def add_scope(scope_name)
57
- @scopes ||= {ROOT => {fields: {}, extends: [], includes: []}}
58
- @scopes[scope_name] = {fields: {}, extends: [], includes: []}
83
+ @scopes ||= {ROOT => new_opts}
84
+ @scopes[scope_name] = new_opts
59
85
  end
60
86
 
61
87
  def add_field(scope, field_name, args, block)
62
- @scopes ||= {ROOT => {fields: {}, extends: [], includes: []}}
63
- @scopes[scope] ||= {fields: {}, extends: [], includes: []}
64
- @scopes[scope][:fields][field_name] = {args: args, block: block}
88
+ @scopes ||= {ROOT => new_opts}
89
+ @scopes[scope] ||= new_opts
90
+ if args[:setter]
91
+ add_setter(scope, field_name, {}, nil)
92
+ end
93
+ @scopes[scope][:fields][field_name] = {args: args.select{ |k| k != :setter}, block: block}
94
+ end
95
+
96
+ def add_setter(scope, field_name, args, block)
97
+ @scopes ||= {ROOT => new_opts}
98
+ @scopes[scope] ||= new_opts
99
+ @scopes[scope][:setters][field_name] = {args: args, block: block}
65
100
  end
66
101
 
67
102
  end
@@ -23,6 +23,18 @@ describe ModelView do
23
23
  expect(scope_fields[:a_field]).to eq({args: {foo: 1}, block: nil})
24
24
  end
25
25
  end
26
+
27
+ context "with setter set to true" do
28
+ it "adds the field and setter to the root scope" do
29
+ dummy_class.field :a_field, {setter: true}
30
+
31
+ scope_fields = dummy_class.scopes[root_scope][:fields]
32
+ expect(scope_fields[:a_field]).to eq({args: {}, block: nil})
33
+
34
+ scope_setters = dummy_class.scopes[root_scope][:setters]
35
+ expect(scope_setters[:a_field]).to eq({args: {}, block: nil})
36
+ end
37
+ end
26
38
  end
27
39
 
28
40
  context "with a block" do
@@ -57,6 +69,37 @@ describe ModelView do
57
69
  expect { dummy_class.extend_scope :a_field }.to raise_error "Root scope can not extend another scope"
58
70
  end
59
71
  end
72
+
73
+ describe :setter do
74
+ context "without a block" do
75
+ it "adds the setter to the root scope" do
76
+ dummy_class.setter :a_field
77
+
78
+ scope_setters = dummy_class.scopes[root_scope][:setters]
79
+ expect(scope_setters[:a_field]).to eq({args: {}, block: nil})
80
+ end
81
+ end
82
+
83
+ context "with a block" do
84
+ it "adds the seter to the root scope" do
85
+ dummy_class.setter :a_field { 1 + 1 }
86
+
87
+ scope_setters = dummy_class.scopes[root_scope][:setters]
88
+
89
+ expect(scope_setters[:a_field][:args]).to eq({})
90
+ expect(scope_setters[:a_field][:block].call).to eq(2)
91
+ end
92
+ end
93
+ end
94
+
95
+ describe :after_update do
96
+ it "adds an after_update block" do
97
+ dummy_class.after_update { |obj| obj.save }
98
+
99
+ after_update = dummy_class.scopes[root_scope][:after_update]
100
+ expect(after_update).to be_a(Proc)
101
+ end
102
+ end
60
103
  end
61
104
 
62
105
  context "within a scope" do
@@ -142,6 +185,29 @@ describe ModelView do
142
185
  end
143
186
  end
144
187
  end
188
+
189
+ describe :setter do
190
+ context "without a block" do
191
+ it "adds the setter to the root scope" do
192
+ dummy_class.scope :my_scope { setter :a_field }
193
+
194
+ scope_setters = dummy_class.scopes[:my_scope][:setters]
195
+ expect(scope_setters[:a_field]).to eq({args: {}, block: nil})
196
+ end
197
+ end
198
+
199
+ context "with a block" do
200
+ it "adds the seter to the root scope" do
201
+ dummy_class.scope :my_scope { setter :a_field { 1 + 1 } }
202
+
203
+ scope_setters = dummy_class.scopes[:my_scope][:setters]
204
+
205
+ expect(scope_setters[:a_field][:args]).to eq({})
206
+ expect(scope_setters[:a_field][:block].call).to eq(2)
207
+ end
208
+ end
209
+ end
210
+
145
211
  end
146
212
 
147
213
  describe :model do
@@ -130,6 +130,5 @@ describe ModelView::Resolver do
130
130
  end
131
131
  end
132
132
 
133
-
134
133
  end
135
134
  end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+ require 'pry'
3
+
4
+ describe ModelView::Updater do
5
+ let(:scopes) do
6
+ {
7
+ __root__: {
8
+ fields: {
9
+ field1: {},
10
+ field2: {},
11
+ field3: {}
12
+ },
13
+ setters: {
14
+ field1: {},
15
+ field3: {block: Proc.new { |obj, data| obj.field3 = data + 2 }}
16
+ },
17
+ after_update: Proc.new { |obj| obj.save }
18
+ },
19
+ scope1: {
20
+ fields: {
21
+ field4: {block: Proc.new { |obj| obj.field4 + obj.field1 }},
22
+ field5: {},
23
+ field6: {}
24
+ },
25
+ setters: {
26
+ field4: {block: Proc.new { |obj, data| obj.field4 = data }},
27
+ field5: {},
28
+ field6: {}
29
+ }
30
+ },
31
+ scope2: {
32
+ extends: [:scope1],
33
+ fields: {
34
+ field7: {},
35
+ field8: {},
36
+ field9: {}
37
+ },
38
+ setters: {
39
+ field7: {}
40
+ }
41
+ },
42
+ scope3: {
43
+ includes: [:scope1],
44
+ fields: {
45
+ field10: {},
46
+ field11: {},
47
+ field12: {}
48
+ },
49
+ setters: {
50
+ field10: {}
51
+ }
52
+ }
53
+ }
54
+ end
55
+
56
+ describe :update do
57
+ before do
58
+ class Dummy
59
+ attr_accessor *(1..12).map { |n| "field#{n}".to_sym }
60
+
61
+ def save
62
+
63
+ end
64
+ end
65
+ end
66
+
67
+ let(:instance) { Dummy.new }
68
+
69
+ context "for the root scope" do
70
+ let(:data) { {field1: 1, field2: 2, field3: 2} }
71
+ context "for a setter without a block" do
72
+ it "sets the values with setters" do
73
+ ModelView::Updater.update(instance, scopes, data)
74
+ expect(instance.field1).to eq(1)
75
+ end
76
+
77
+ it "does not set the values without setters" do
78
+ ModelView::Updater.update(instance, scopes, data)
79
+ expect(instance.field2).to be_nil
80
+ end
81
+
82
+ it "uses a block to set the value, if available" do
83
+ ModelView::Updater.update(instance, scopes, data)
84
+ expect(instance.field3).to eq(4)
85
+ end
86
+
87
+ it "runs the after_update block, if provided" do
88
+ expect(instance).to receive(:save)
89
+ ModelView::Updater.update(instance, scopes, data)
90
+ end
91
+ end
92
+ end
93
+
94
+ context "on a scope" do
95
+ it "includes the root level setters" do
96
+ data = {field1: 1, field4: 4}
97
+ ModelView::Updater.update(instance, scopes, data, :scope1)
98
+
99
+ expect(instance.field1).to eq(1)
100
+ expect(instance.field4).to eq(4)
101
+ end
102
+ end
103
+
104
+ context "on a scope extending another scope" do
105
+ it "also includes the extended scope's setters" do
106
+ data = {field7: 7, field4: 4}
107
+ ModelView::Updater.update(instance, scopes, data, :scope2)
108
+
109
+ expect(instance.field7).to eq(7)
110
+ expect(instance.field4).to eq(4)
111
+ end
112
+ end
113
+
114
+ context "on a scope including another scope" do
115
+ it "also includes the included scope's setters"
116
+ end
117
+
118
+ end
119
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Pretorius
@@ -96,10 +96,12 @@ files:
96
96
  - README.md
97
97
  - lib/model_view.rb
98
98
  - lib/model_view/resolver.rb
99
+ - lib/model_view/updater.rb
99
100
  - lib/model_view/version.rb
100
101
  - model_view.gemspec
101
102
  - spec/model_view/model_view_spec.rb
102
103
  - spec/model_view/resolver_spec.rb
104
+ - spec/model_view/updater_spec.rb
103
105
  - spec/spec_helper.rb
104
106
  homepage: https://github.com/Offerzen/model_view
105
107
  licenses:
@@ -128,4 +130,5 @@ summary: Composable serialisation for models
128
130
  test_files:
129
131
  - spec/model_view/model_view_spec.rb
130
132
  - spec/model_view/resolver_spec.rb
133
+ - spec/model_view/updater_spec.rb
131
134
  - spec/spec_helper.rb