smooth_operator 0.4.4 → 1.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 +9 -9
- data/.gitignore +2 -1
- data/.rspec +4 -0
- data/Gemfile +13 -0
- data/README.md +258 -10
- data/console.rb +44 -0
- data/lib/smooth_operator/array_with_meta_data.rb +31 -0
- data/lib/smooth_operator/attribute_assignment.rb +102 -0
- data/lib/smooth_operator/attribute_methods.rb +87 -0
- data/lib/smooth_operator/attributes/base.rb +107 -0
- data/lib/smooth_operator/attributes/dirty.rb +29 -0
- data/lib/smooth_operator/attributes/normal.rb +15 -0
- data/lib/smooth_operator/delegation.rb +60 -0
- data/lib/smooth_operator/finder_methods.rb +43 -0
- data/lib/smooth_operator/helpers.rb +79 -0
- data/lib/smooth_operator/model_schema.rb +81 -0
- data/lib/smooth_operator/open_struct.rb +37 -0
- data/lib/smooth_operator/operator.rb +145 -0
- data/lib/smooth_operator/operators/faraday.rb +75 -0
- data/lib/smooth_operator/operators/typhoeus.rb +77 -0
- data/lib/smooth_operator/persistence.rb +144 -0
- data/lib/smooth_operator/relation/array_relation.rb +13 -0
- data/lib/smooth_operator/relation/association_reflection.rb +75 -0
- data/lib/smooth_operator/relation/associations.rb +75 -0
- data/lib/smooth_operator/relation/reflection.rb +41 -0
- data/lib/smooth_operator/relation/single_relation.rb +14 -0
- data/lib/smooth_operator/remote_call/base.rb +80 -0
- data/lib/smooth_operator/remote_call/errors/connection_failed.rb +20 -0
- data/lib/smooth_operator/remote_call/errors/timeout.rb +20 -0
- data/lib/smooth_operator/remote_call/faraday.rb +19 -0
- data/lib/smooth_operator/remote_call/typhoeus.rb +19 -0
- data/lib/smooth_operator/serialization.rb +79 -0
- data/lib/smooth_operator/translation.rb +27 -0
- data/lib/smooth_operator/validations.rb +15 -0
- data/lib/smooth_operator/version.rb +1 -1
- data/lib/smooth_operator.rb +26 -5
- data/smooth_operator.gemspec +12 -3
- data/spec/factories/user_factory.rb +34 -0
- data/spec/require_helper.rb +11 -0
- data/spec/smooth_operator/attribute_assignment_spec.rb +351 -0
- data/spec/smooth_operator/attributes_dirty_spec.rb +53 -0
- data/spec/smooth_operator/delegation_spec.rb +139 -0
- data/spec/smooth_operator/finder_methods_spec.rb +105 -0
- data/spec/smooth_operator/model_schema_spec.rb +31 -0
- data/spec/smooth_operator/operator_spec.rb +46 -0
- data/spec/smooth_operator/persistence_spec.rb +424 -0
- data/spec/smooth_operator/remote_call_spec.rb +320 -0
- data/spec/smooth_operator/serialization_spec.rb +80 -0
- data/spec/smooth_operator/validations_spec.rb +42 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/helpers/persistence_helper.rb +38 -0
- data/spec/support/localhost_server.rb +97 -0
- data/spec/support/models/address.rb +14 -0
- data/spec/support/models/comment.rb +3 -0
- data/spec/support/models/post.rb +13 -0
- data/spec/support/models/user.rb +41 -0
- data/spec/support/models/user_with_address_and_posts.rb +89 -0
- data/spec/support/test_server.rb +165 -0
- metadata +108 -18
- data/lib/smooth_operator/base.rb +0 -30
- data/lib/smooth_operator/core.rb +0 -218
- data/lib/smooth_operator/http_handlers/typhoeus/base.rb +0 -58
- data/lib/smooth_operator/http_handlers/typhoeus/orm.rb +0 -34
- data/lib/smooth_operator/http_handlers/typhoeus/remote_call.rb +0 -28
- data/lib/smooth_operator/operator/base.rb +0 -43
- data/lib/smooth_operator/operator/exceptions.rb +0 -64
- data/lib/smooth_operator/operator/orm.rb +0 -118
- data/lib/smooth_operator/operator/remote_call.rb +0 -84
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZDVjYjVmMmQ5NDBjMjM3ZDFhODA0Y2FlODhmNzAxYmU2ZjJhMWU3Nw==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
7
|
-
|
6
|
+
MjhlNzJlYTA1YTc4OTgxYzlkZTUyOTczN2M3NDY2NzdhZGQ0YzczZQ==
|
7
|
+
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YzgxZjhhZTYzNjMxZmZiYmIyOWUzODAzYjg4OGUwYzgxOTRiMmUxMjJhZTIx
|
10
|
+
ODE1NTRhYTg2NmE4OTY1ZGExNzUyYzFjZTM5NjU2ZjljOWJiMGExNmMxNmJj
|
11
|
+
YTEyYmY0NDAxMzc4ZTQxNmJlYjMyMGM1ODJmOThhOTdhNTcxYzI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YmNiZThmOGQyZTI5OWU5MTZiMTAwMjNjOTI5YTdhNmEzZWNhMTdjM2MwNDgz
|
14
|
+
Nzg1MzM2ZWRlODEwYmY1ZDY2OGRiNTBkMDhiYmJiM2UwMDAxZjVhN2JkYTQ0
|
15
|
+
NjNiNGU1N2ZkYWEwMWI3YmY4NGMyNjU5NWQ2MDkwMjJlZTdlNzk=
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Gemfile
CHANGED
@@ -2,3 +2,16 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in smooth_operator.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
gem 'simplecov', :require => false, :group => :test
|
7
|
+
|
8
|
+
group :development, :test do
|
9
|
+
gem "pry"
|
10
|
+
gem "sinatra"
|
11
|
+
gem "typhoeus"
|
12
|
+
gem "activesupport"
|
13
|
+
gem "sinatra-contrib"
|
14
|
+
gem "rspec", "~> 3.0.0.beta1"
|
15
|
+
gem "factory_girl", "~> 4.0"
|
16
|
+
gem "ethon", :git => 'https://github.com/goncalvesjoao/ethon'
|
17
|
+
end
|
data/README.md
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
# SmoothOperator
|
2
2
|
|
3
|
-
|
3
|
+
Ruby gem, that mimics the ActiveRecord behaviour but through external API's.
|
4
|
+
It's a lightweight and flexible alternative to ActiveResource, that responds to a REST API like you expect it too.
|
4
5
|
|
5
|
-
|
6
|
+
Depends only on Faraday gem, no need for ActiveSupport or any other Active* gem.
|
7
|
+
|
8
|
+
Although if I18n is present it will respond to .human_attribute_name method and if ActiveModel is present it will make use of 'ActiveModel::Name' to improve .model_name method.
|
9
|
+
|
10
|
+
---
|
11
|
+
|
12
|
+
## 1) Installation
|
6
13
|
|
7
14
|
Add this line to your application's Gemfile:
|
8
15
|
|
@@ -16,14 +23,255 @@ Or install it yourself as:
|
|
16
23
|
|
17
24
|
$ gem install smooth_operator
|
18
25
|
|
19
|
-
|
26
|
+
---
|
27
|
+
|
28
|
+
## 2) Usage and Examples
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class MyBlogResource < SmoothOperator::Base
|
32
|
+
self.endpoint = 'http://myblog.com/api/v0'
|
33
|
+
|
34
|
+
# HTTP BASIC AUTH
|
35
|
+
self.endpoint_user = 'admin'
|
36
|
+
self.endpoint_pass = 'admin'
|
37
|
+
end
|
38
|
+
|
39
|
+
class Post < MyBlogResource
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
---
|
44
|
+
|
45
|
+
### 2.1) Creating a .new 'Post' and #save it
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
post = Post.new(body: 'my first post', author: 'John Doe')
|
49
|
+
|
50
|
+
post.new_record? # true
|
51
|
+
post.persisted? # false
|
52
|
+
post.body # 'my first post'
|
53
|
+
post.author # 'John Doe'
|
54
|
+
post.something_else # will raise NoMethodError
|
55
|
+
|
56
|
+
save_result = post.save # will make a http POST call to 'http://myblog.com/api/v0/posts'
|
57
|
+
# with `{ post: { body: 'my first post', author: 'John Doe' } }`
|
58
|
+
|
59
|
+
post.last_remote_call # will contain a SmoothOperator::RemoteCall instance containing relevant information about the save remote call.
|
60
|
+
|
61
|
+
# If the server response is positive (http code between 200 and 299):
|
62
|
+
save_result # true
|
63
|
+
post.new_record? # false
|
64
|
+
post.persisted? # true
|
65
|
+
# server response contains { id: 1 } on its body
|
66
|
+
post.id # 1
|
67
|
+
|
68
|
+
# If the server response is negative (http code between 400 and 499):
|
69
|
+
save_result # false
|
70
|
+
post.new_record? # true
|
71
|
+
post.persisted? # false
|
72
|
+
# server response contains { errors: { body: ['must be less then 10 letters'] } }
|
73
|
+
post.errors.body # Array
|
74
|
+
|
75
|
+
# If the server response is an error (http code between 500 and 599), or the connection was broke:
|
76
|
+
save_result # nil
|
77
|
+
post.new_record? # true
|
78
|
+
post.persisted? # false
|
79
|
+
# server response contains { errors: { body: ['must be less then 10 letters'] } }
|
80
|
+
post.errors # will raise NoMethodError
|
81
|
+
|
82
|
+
# In the positive and negative server response comes with a json,
|
83
|
+
# e.g. { id: 1 }, post will reflect that new data
|
84
|
+
post.id # 1
|
85
|
+
|
86
|
+
# In case of error and the server response contains a json,
|
87
|
+
# e.g. { id: 1 }, post will NOT reflect that data
|
88
|
+
post.id # raise NoMethodError
|
89
|
+
|
90
|
+
```
|
91
|
+
|
92
|
+
---
|
93
|
+
|
94
|
+
### 2.2) Editing an existing record
|
95
|
+
```ruby
|
96
|
+
post = Post.find(2)
|
97
|
+
|
98
|
+
post.body = 'editing my second page'
|
99
|
+
|
100
|
+
post.save
|
101
|
+
```
|
102
|
+
|
103
|
+
---
|
104
|
+
|
105
|
+
### 2.3) Customize #save 'url', 'params' and 'options'
|
106
|
+
```ruby
|
107
|
+
post = Post.new(id: 2, body: 'editing my second page')
|
108
|
+
|
109
|
+
post.new_record? # false
|
110
|
+
post.persisted? # true
|
111
|
+
|
112
|
+
post.save("#{post.id}/save_and_add_to_list", { admin: true, post: { author: 'Agent Smith', list_id: 1 } }, { timeout: 1 })
|
113
|
+
# Will make a PUT to 'http://myblog.com/api/v0/posts/2/save_and_add_to_list'
|
114
|
+
# with { admin: true, post: { body: 'editing my second page', list_id: 1 } }
|
115
|
+
# and will only wait 1sec for the server to respond.
|
116
|
+
```
|
117
|
+
|
118
|
+
---
|
119
|
+
|
120
|
+
### 2.4) Saving using HTTP Patch verb
|
121
|
+
```ruby
|
122
|
+
class Page < MyBlogResource
|
123
|
+
self.update_http_verb = :patch
|
124
|
+
end
|
125
|
+
|
126
|
+
page = Page.find(2)
|
127
|
+
|
128
|
+
page.body = 'editing my second page'
|
129
|
+
|
130
|
+
page.save # will make a http PATCH call to 'http://myblog.com/api/v0/pages/2'
|
131
|
+
# with `{ page: { body: 'editing my second page' } }`
|
132
|
+
```
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
### 2.5) Retrieving remote objects - 'index' REST action
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
remote_call = Page.find(:all) # Will make a GET call to 'http://myblog.com/api/v0/pages'
|
140
|
+
# and will return a SmoothOperator::RemoteCall instance
|
141
|
+
|
142
|
+
pages = remote_call.objects # 'pages = remote_call.data' also works
|
143
|
+
|
144
|
+
# If the server response is positive (http code between 200 and 299):
|
145
|
+
remote_call.ok? # true
|
146
|
+
remote_call.not_processed? # false
|
147
|
+
remote_call.error? # false
|
148
|
+
remote_call.status # true
|
149
|
+
pages = remote_call.data # array of Page instances
|
150
|
+
remote_call.http_status # server_response code
|
151
|
+
|
152
|
+
# If the server response is unprocessed entity (http code 422):
|
153
|
+
remote_call.ok? # false
|
154
|
+
remote_call.not_processed? # true
|
155
|
+
remote_call.error? # false
|
156
|
+
remote_call.status # false
|
157
|
+
remote_call.http_status # server_response code
|
158
|
+
|
159
|
+
# If the server response is client error (http code between 400..499, except 422):
|
160
|
+
remote_call.ok? # false
|
161
|
+
remote_call.not_processed? # false
|
162
|
+
remote_call.error? # true
|
163
|
+
remote_call.status # nil
|
164
|
+
remote_call.http_status # server_response code
|
165
|
+
|
166
|
+
# If the server response is server error (http code between 500 and 599), or the connection broke:
|
167
|
+
remote_call.ok? # false
|
168
|
+
remote_call.not_processed? # false
|
169
|
+
remote_call.error? # true
|
170
|
+
remote_call.status # nil
|
171
|
+
remote_call.http_status # server_response code or 0 if connection broke
|
172
|
+
```
|
173
|
+
|
174
|
+
---
|
175
|
+
|
176
|
+
### 2.6) Retrieving remote objects - 'show' REST action
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
remote_call = Page.find(2) # Will make a GET call to 'http://myblog.com/api/v0/pages/2'
|
180
|
+
# and will return a SmoothOperator::RemoteCall instance
|
181
|
+
|
182
|
+
page = remote_call.object # 'page = remote_call.data' also works
|
183
|
+
```
|
184
|
+
|
185
|
+
---
|
186
|
+
|
187
|
+
### 2.7) Retrieving remote objects - custom query
|
188
|
+
```ruby
|
189
|
+
remote_call = Page.find('my_pages', { q: body_contains: 'link' }, { endpoint_user: 'admin', endpoint_pass: 'new_password' })
|
190
|
+
# will make a GET call to 'http://myblog.com/api/v0/pages/my_pages?q={body_contains="link"}'
|
191
|
+
# and will change the HTTP BASIC AUTH credentials to user: 'admin' and pass: 'new_password' for this connection only.
|
192
|
+
|
193
|
+
# If the server json response is an Array [{ id: 1 }, { id: 2 }]
|
194
|
+
pages = remote.data # will return an array with 2 Page's instances
|
195
|
+
pages[0].id # 1
|
196
|
+
pages[1].id # 2
|
197
|
+
|
198
|
+
# If the server json response is a Hash { id: 3 }
|
199
|
+
page = remote.data # will return a single Page instance
|
200
|
+
page.id # 3
|
201
|
+
|
202
|
+
# If the server json response is Hash with a key called 'pages' { page: 1, total: 3, pages: [{ id: 4 }, { id: 5 }] }
|
203
|
+
pages = remote.data # will return a single ArrayWithMetaData instance, that will allow you to access to both the Page's instances array and the metadata.
|
204
|
+
pages.page # 1
|
205
|
+
pages.total # 3
|
206
|
+
|
207
|
+
pages[0].id # 4
|
208
|
+
pages[1].id # 5
|
209
|
+
```
|
210
|
+
|
211
|
+
---
|
212
|
+
|
213
|
+
## 3) Methods
|
214
|
+
|
215
|
+
---
|
216
|
+
|
217
|
+
### 3.1) Persistence methods
|
218
|
+
|
219
|
+
Methods | Behaviour | Arguments | Return
|
220
|
+
------- | --------- | ------ | ---------
|
221
|
+
.create | Generates a new instance of the class with *attributes and calls #save with the rest of its arguments| Hash attributes = nil, String relative_path = nil, Hash data = {}, Hash options = {} | Class instance
|
222
|
+
#new_record? | Returns @new_record if defined, else populates it with true if #id is present or false if blank. | - | Boolean
|
223
|
+
#destroyed?| Returns @destroyed if defined, else populates it with false. | - | Boolean
|
224
|
+
#persisted?| Returns true if both #new_record? and #destroyed? return false, else returns false. | - | Boolean
|
225
|
+
#save | if #new_record? makes a HTTP POST, else a PUT call. If !#new_record? and relative_path is blank, sets relative_path = id.to_s. If the server POST response is positive, sets @new_record = false. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
|
226
|
+
#save! | Executes the same behaviour as #save, but will raise RecordNotSaved if the returning value is not true | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
|
227
|
+
#destroy | Does nothing if !persisted? else makes a HTTP DELETE call. If server response it positive, sets @destroyed = true. If relative_path is blank, sets relative_path = id.to_s. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
|
228
|
+
|
229
|
+
---
|
230
|
+
|
231
|
+
### 3.2) Finder methods
|
232
|
+
|
233
|
+
Methods | Behaviour | Arguments | Return
|
234
|
+
------- | --------- | ------ | ---------
|
235
|
+
.find | If relative_path == :all, sets relative_path = ''. Makes a Get call and initiates Class objects with the server's response data. See 4.3) and 4.4) for more behaviour info. | String relative_path, Hash data = {}, Hash options = {} | Class instance, Array of Class instances or an ArrayWithMetaData instance
|
236
|
+
|
237
|
+
---
|
238
|
+
|
239
|
+
### 3.3) Operator methods
|
240
|
+
...
|
241
|
+
|
242
|
+
---
|
243
|
+
|
244
|
+
### 3.3) Remote call methods
|
245
|
+
...
|
246
|
+
|
247
|
+
---
|
248
|
+
|
249
|
+
## 4) Behaviours
|
250
|
+
|
251
|
+
---
|
252
|
+
|
253
|
+
### 4.1) Delegation behaviour
|
254
|
+
...
|
255
|
+
|
256
|
+
---
|
257
|
+
|
258
|
+
### 4.2) Persistent operator behaviour
|
259
|
+
...
|
260
|
+
|
261
|
+
---
|
262
|
+
|
263
|
+
### 4.3) Operator behaviour
|
264
|
+
...
|
265
|
+
|
266
|
+
---
|
267
|
+
|
268
|
+
### 4.4) Remote call behaviour
|
269
|
+
...
|
20
270
|
|
21
|
-
|
271
|
+
---
|
22
272
|
|
23
|
-
##
|
273
|
+
## 4) TODO
|
24
274
|
|
25
|
-
1.
|
26
|
-
2.
|
27
|
-
3.
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create new Pull Request
|
275
|
+
1. Finish "Methods" and "Behaviours" documentation;
|
276
|
+
2. ModelSchema specs;
|
277
|
+
3. Cache.
|
data/console.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << './'
|
4
|
+
$LOAD_PATH << './lib'
|
5
|
+
|
6
|
+
require 'spec/require_helper'
|
7
|
+
|
8
|
+
FactoryGirl.find_definitions
|
9
|
+
|
10
|
+
LocalhostServer.new(TestServer.new, 4567)
|
11
|
+
|
12
|
+
# user = nil
|
13
|
+
|
14
|
+
# hydra = Typhoeus::Hydra::hydra
|
15
|
+
|
16
|
+
# user = User::Base.new(id: 1)
|
17
|
+
# user.reload(nil, nil, { hydra: hydra })
|
18
|
+
|
19
|
+
# User::Base.find(1, nil, { hydra: hydra }) do |remote_call|
|
20
|
+
# user = remote_call.data
|
21
|
+
# end
|
22
|
+
|
23
|
+
#User::Base.post('', { user: { age: 1, posts: [{ body: 'post1' }, 2] } })
|
24
|
+
|
25
|
+
#user = UserWithAddressAndPosts::Son.new(FactoryGirl.attributes_for(:user_with_address_and_posts))
|
26
|
+
#user.save('', { status: 200 })
|
27
|
+
|
28
|
+
# "[{\"patient_id\"=>33, \"messages\"=>[{\"id\"=>\"53722c20cb38247c36000003\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:28:48Z\"}, {\"id\"=>\"53722bfccb382485d5000002\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:28:12Z\"}, {\"id\"=>\"53722b91cb3824e913000001\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:26:25Z\"}]}]"
|
29
|
+
|
30
|
+
post = Post.new(comments: [{ id: 1, name: '1' }, { id: 2, name: '2' }], address: { id: 1, name: 'address' })
|
31
|
+
|
32
|
+
class Test < SimpleDelegator
|
33
|
+
def reload
|
34
|
+
"TODO"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Test2 < Test
|
39
|
+
def reload2
|
40
|
+
"TODO2"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
binding.pry
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SmoothOperator
|
2
|
+
class ArrayWithMetaData < OpenStruct::Base
|
3
|
+
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :meta_data, :internal_array
|
9
|
+
|
10
|
+
def_delegators :internal_array, :length, :<<, :[]
|
11
|
+
|
12
|
+
def initialize(attributes, object_class)
|
13
|
+
_attributes, _resources_name = attributes.dup, object_class.resources_name
|
14
|
+
|
15
|
+
@internal_array = [*_attributes[_resources_name]].map { |array_entry| object_class.new(array_entry).tap { |object| object.reloaded = true } }
|
16
|
+
_attributes.delete(_resources_name)
|
17
|
+
|
18
|
+
@meta_data = _attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
internal_array.each { |array_entry| yield array_entry }
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
_method = method.to_s
|
27
|
+
meta_data.include?(_method) ? meta_data[_method] : super
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'smooth_operator/attributes/base'
|
2
|
+
require 'smooth_operator/attributes/dirty'
|
3
|
+
require 'smooth_operator/attributes/normal'
|
4
|
+
|
5
|
+
module SmoothOperator
|
6
|
+
|
7
|
+
module AttributeAssignment
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
attr_writer :unknown_hash_class
|
16
|
+
|
17
|
+
def unknown_hash_class
|
18
|
+
Helpers.get_instance_variable(self, :unknown_hash_class, ::OpenStruct)
|
19
|
+
end
|
20
|
+
|
21
|
+
def attributes_white_list
|
22
|
+
Helpers.get_instance_variable(self, :attributes_white_list, Set.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes_black_list
|
26
|
+
Helpers.get_instance_variable(self, :attributes_black_list, Set.new)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes_white_list_add(*getters)
|
30
|
+
attributes_white_list.merge getters.map(&:to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes_black_list_add(*getters)
|
34
|
+
attributes_black_list.merge getters.map(&:to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def dirty_attributes
|
38
|
+
@dirty_attributes = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def dirty_attributes?
|
42
|
+
@dirty_attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(attributes = {}, options = {})
|
48
|
+
@_options = {}
|
49
|
+
|
50
|
+
before_initialize(attributes, options)
|
51
|
+
|
52
|
+
assign_attributes attributes, options
|
53
|
+
|
54
|
+
after_initialize(attributes, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :_options, :_meta_data
|
58
|
+
|
59
|
+
|
60
|
+
def assign_attributes(_attributes = {}, options = {})
|
61
|
+
return nil unless _attributes.is_a?(Hash)
|
62
|
+
|
63
|
+
attributes = _attributes = Helpers.stringify_keys(_attributes)
|
64
|
+
|
65
|
+
if _attributes.include?(self.class.resource_name)
|
66
|
+
attributes = _attributes.delete(self.class.resource_name)
|
67
|
+
@_meta_data = _attributes
|
68
|
+
end
|
69
|
+
|
70
|
+
options.each { |key, value| @_options[key] = value } if options.is_a?(Hash)
|
71
|
+
|
72
|
+
attributes.each { |name, value| push_to_internal_data(name, value, true) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def parent_object
|
76
|
+
_options[:parent_object]
|
77
|
+
end
|
78
|
+
|
79
|
+
def has_data_from_server
|
80
|
+
_options[:from_server] == true
|
81
|
+
end
|
82
|
+
|
83
|
+
alias :from_server :has_data_from_server
|
84
|
+
|
85
|
+
protected #################### PROTECTED METHODS DOWN BELOW ######################
|
86
|
+
|
87
|
+
def before_initialize(attributes, options); end
|
88
|
+
|
89
|
+
def after_initialize(attributes, options); end
|
90
|
+
|
91
|
+
def allowed_attribute(attribute)
|
92
|
+
if !self.class.attributes_white_list.empty?
|
93
|
+
self.class.attributes_white_list.include?(attribute)
|
94
|
+
elsif !self.class.attributes_black_list.empty?
|
95
|
+
!self.class.attributes_black_list.include?(attribute)
|
96
|
+
else
|
97
|
+
true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module SmoothOperator
|
2
|
+
module AttributeMethods
|
3
|
+
|
4
|
+
def internal_data
|
5
|
+
@internal_data ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_internal_data(field, method = :value)
|
9
|
+
result = internal_data[field]
|
10
|
+
|
11
|
+
if result.nil?
|
12
|
+
nil
|
13
|
+
elsif method == :value
|
14
|
+
result.is_a?(Attributes::Dirty) ? internal_data[field].send(method) : internal_data[field]
|
15
|
+
else
|
16
|
+
internal_data[field].send(method)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_attribute_type(attribute)
|
21
|
+
self.class.internal_structure[attribute.to_s]
|
22
|
+
end
|
23
|
+
|
24
|
+
def push_to_internal_data(attribute_name, attribute_value, cast = false)
|
25
|
+
attribute_name = attribute_name.to_s
|
26
|
+
|
27
|
+
return nil unless allowed_attribute(attribute_name)
|
28
|
+
|
29
|
+
known_attributes.add attribute_name
|
30
|
+
|
31
|
+
if internal_data[attribute_name].nil?
|
32
|
+
initiate_internal_data(attribute_name, attribute_value, cast)
|
33
|
+
else
|
34
|
+
update_internal_data(attribute_name, attribute_value, cast)
|
35
|
+
end
|
36
|
+
|
37
|
+
if self.class.respond_to?(:smooth_operator?)
|
38
|
+
trigger_necessary_events(attribute_name, attribute_value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def column_for_attribute(attribute_name)
|
43
|
+
if defined?(ActiveRecord)
|
44
|
+
type = get_attribute_type(attribute_name)
|
45
|
+
ActiveRecord::ConnectionAdapters::Column.new(attribute_name.to_sym, type, type)
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected #################### PROTECTED METHODS DOWN BELOW ######################
|
52
|
+
|
53
|
+
def trigger_necessary_events(attribute_name, attribute_value)
|
54
|
+
mark_for_destruction?(attribute_value) if attribute_name == self.class.destroy_key
|
55
|
+
|
56
|
+
new_record?(true) if attribute_name == self.class.primary_key
|
57
|
+
end
|
58
|
+
|
59
|
+
private ######################## PRIVATE #############################
|
60
|
+
|
61
|
+
def initiate_internal_data(attribute_name, attribute_value, cast)
|
62
|
+
if cast
|
63
|
+
internal_data[attribute_name] = new_attribute_object(attribute_name, attribute_value)
|
64
|
+
|
65
|
+
internal_data[attribute_name] = internal_data[attribute_name].value unless self.class.dirty_attributes?
|
66
|
+
else
|
67
|
+
internal_data[attribute_name] = attribute_value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_internal_data(attribute_name, attribute_value, cast)
|
72
|
+
if self.class.dirty_attributes?
|
73
|
+
internal_data[attribute_name].set_value(attribute_value, self)
|
74
|
+
else
|
75
|
+
internal_data[attribute_name] = cast ? new_attribute_object(attribute_name, attribute_value).value : attribute_value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def new_attribute_object(attribute_name, attribute_value)
|
80
|
+
attribute_class = self.class.dirty_attributes? ? Attributes::Dirty : Attributes::Normal
|
81
|
+
|
82
|
+
attribute_class.new(attribute_name, attribute_value, self)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|