smooth_operator 0.4.4 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|