lhs 2.2.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +296 -30
  3. data/lhs.gemspec +3 -3
  4. data/lib/lhs.rb +0 -7
  5. data/lib/lhs/collection.rb +4 -4
  6. data/lib/lhs/concerns/item/destroy.rb +2 -2
  7. data/lib/lhs/concerns/item/save.rb +3 -3
  8. data/lib/lhs/concerns/item/update.rb +2 -2
  9. data/lib/lhs/concerns/item/validation.rb +4 -5
  10. data/lib/lhs/concerns/{service → record}/all.rb +2 -2
  11. data/lib/lhs/concerns/{service → record}/batch.rb +3 -3
  12. data/lib/lhs/concerns/{service → record}/create.rb +4 -3
  13. data/lib/lhs/concerns/{service → record}/endpoints.rb +6 -6
  14. data/lib/lhs/concerns/{service → record}/find.rb +3 -2
  15. data/lib/lhs/concerns/{service → record}/find_by.rb +3 -2
  16. data/lib/lhs/concerns/{service → record}/first.rb +1 -1
  17. data/lib/lhs/concerns/{service → record}/includes.rb +4 -2
  18. data/lib/lhs/concerns/{service → record}/mapping.rb +1 -1
  19. data/lib/lhs/concerns/{service → record}/model.rb +1 -1
  20. data/lib/lhs/concerns/{service → record}/request.rb +12 -12
  21. data/lib/lhs/concerns/{service → record}/where.rb +3 -2
  22. data/lib/lhs/data.rb +25 -53
  23. data/lib/lhs/endpoint.rb +2 -2
  24. data/lib/lhs/item.rb +8 -2
  25. data/lib/lhs/proxy.rb +2 -2
  26. data/lib/lhs/record.rb +42 -0
  27. data/lib/lhs/version.rb +1 -1
  28. data/spec/collection/meta_data_spec.rb +2 -2
  29. data/spec/collection/without_object_items_spec.rb +1 -1
  30. data/spec/data/item_spec.rb +2 -14
  31. data/spec/data/merge_spec.rb +3 -3
  32. data/spec/data/raw_spec.rb +1 -1
  33. data/spec/data/respond_to_spec.rb +3 -3
  34. data/spec/data/root_spec.rb +2 -2
  35. data/spec/data/to_json_spec.rb +2 -2
  36. data/spec/endpoint/for_url_spec.rb +1 -1
  37. data/spec/item/delegate_spec.rb +2 -2
  38. data/spec/item/destroy_spec.rb +4 -4
  39. data/spec/item/errors_spec.rb +4 -4
  40. data/spec/item/getter_spec.rb +2 -2
  41. data/spec/item/internal_data_structure_spec.rb +1 -1
  42. data/spec/item/save_spec.rb +3 -4
  43. data/spec/item/setter_spec.rb +2 -2
  44. data/spec/item/update_spec.rb +2 -2
  45. data/spec/item/validation_spec.rb +47 -75
  46. data/spec/proxy/load_spec.rb +2 -2
  47. data/spec/{service → record}/all_spec.rb +7 -7
  48. data/spec/{service → record}/build_spec.rb +5 -4
  49. data/spec/{service → record}/create_spec.rb +6 -5
  50. data/spec/{service → record}/creation_failed_spec.rb +6 -5
  51. data/spec/record/definitions_spec.rb +29 -0
  52. data/spec/{service → record}/endpoint_misconfiguration_spec.rb +3 -3
  53. data/spec/{service → record}/endpoint_options_spec.rb +4 -4
  54. data/spec/{service → record}/endpoints_spec.rb +15 -15
  55. data/spec/{service → record}/find_by_spec.rb +8 -8
  56. data/spec/{service → record}/find_each_spec.rb +3 -3
  57. data/spec/{service → record}/find_in_batches_spec.rb +6 -6
  58. data/spec/{service → record}/find_spec.rb +10 -9
  59. data/spec/{service → record}/first_spec.rb +7 -6
  60. data/spec/{service → record}/includes_spec.rb +7 -7
  61. data/spec/{service → record}/mapping_spec.rb +27 -16
  62. data/spec/{service → record}/model_name_spec.rb +2 -2
  63. data/spec/{service → record}/new_spec.rb +4 -3
  64. data/spec/{service → record}/request_spec.rb +3 -3
  65. data/spec/record/where_spec.rb +34 -0
  66. data/spec/support/cleanup_endpoints.rb +1 -1
  67. data/spec/support/{cleanup_services.rb → cleanup_records.rb} +2 -2
  68. metadata +60 -67
  69. data/docs/collections.md +0 -28
  70. data/docs/data.md +0 -39
  71. data/docs/items.md +0 -79
  72. data/docs/service.jpg +0 -0
  73. data/docs/service.pdf +2 -650
  74. data/docs/services.md +0 -266
  75. data/lib/lhs/concerns/service/build.rb +0 -19
  76. data/lib/lhs/service.rb +0 -18
  77. data/spec/data/pagination_spec.rb +0 -60
  78. data/spec/dummy/README.rdoc +0 -28
  79. data/spec/service/where_spec.rb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d30520e791c021241701581bb76962de5bbb8e72
4
- data.tar.gz: 23d3cbcf4ba46082b30e655aeb40af7d8680151e
3
+ metadata.gz: 23beb0804ec8b041eaa4e920e395a5d9b2bf394f
4
+ data.tar.gz: 3329ba17fc8d206210d143a8b8ad4aae09561316
5
5
  SHA512:
6
- metadata.gz: 3ecc85bf22592f7b5bed6dd2f82180ec1bdb203bb5578ba9905fd11b1ba50f03c2e5be6b44c7c11c1b246c1f1c40205b47e66171ec43b334dbca237ed89b5a91
7
- data.tar.gz: 7a5182fa1eae09ba69ecd5976a93e91c3c910fa132b1d7f8d47544bf4f5c3fc176834b5d0b570dc5f1142af142c0514d63cd590ae5e53e206038cbdecb37276b
6
+ metadata.gz: 451e95f0aa50d9f41eedd21f7339f36904d146f36c3ca880effe675f5b9b7c6a6198592e4a4d6e2c284a10fadbe4b7aa8a96b3f1e60748fae1988bc4e805f788
7
+ data.tar.gz: df8f2a78480a2ead8098c786e04f620e6541420c78803adaf2ee2524c31274a2ddd756d6dd4b7309d08c76cacea9bda907c797a1b6a346e3c2ecdf59a04ae45b
data/README.md CHANGED
@@ -3,65 +3,331 @@ LHS
3
3
 
4
4
  LHS uses [LHC](//github.com/local-ch/LHC) for http requests.
5
5
 
6
- ## Service
7
- A service connects your application to backend endpoints and provides you access to their data.
6
+ ## Very Short Introduction
7
+
8
+ A LHS::Record makes data available using backend services and one or multiple endpoints.
8
9
 
9
10
  ```ruby
10
- class Feedback < LHS::Service
11
+ class Feedback < LHS::Record
11
12
 
12
13
  endpoint ':datastore/v2/content-ads/:campaign_id/feedbacks'
13
14
  endpoint ':datastore/v2/feedbacks'
14
15
 
15
16
  end
16
17
 
17
- data = Feedback.where(has_reviews: true) #<LHS::Data>
18
+ feedback = Feedback.find_by_email('somebody@mail.com') #<Feedback>
19
+ feedback.review # "Lunch was great"
18
20
  ```
19
21
 
20
- [Read more about services](docs/services.md)
22
+ ## Where to store LHS::Records
23
+
24
+ Please store all defined LHS::Records in `app/models` as they are not autoloaded by rails otherwise.
25
+
26
+ ## Endpoints
27
+
28
+ You setup a LHS::Record by configuring one or multiple backend endpoints. You can also add request options for an endpoint (see following example).
29
+
30
+ ```ruby
31
+ class Feedback < LHS::Record
32
+
33
+ endpoint ':datastore/v2/content-ads/:campaign_id/feedbacks'
34
+ endpoint ':datastore/v2/content-ads/:campaign_id/feedbacks/:id'
35
+ endpoint ':datastore/v2/feedbacks', cache: true, cache_expires_in: 1.day
36
+ endpoint ':datastore/v2/feedbacks/:id', cache: true, cache_expires_in: 1.day
37
+
38
+ end
39
+ ```
21
40
 
22
- ## Data
23
- An instance of LHS::Data contains raw data (json) and a proxy that is used to access data.
41
+ If you try to setup a LHS::Record with clashing endpoints it will immediately raise an exception.
24
42
 
25
43
  ```ruby
26
- Service.where #<LHS::Data @_proxy_=#<LHS::Collection>>
27
- Service.find(123) #<LHS::Data @_proxy_=#<LHS::Item>>
44
+ class Feedback < LHS::Record
45
+
46
+ endpoint ':datastore/v2/reviews'
47
+ endpoint ':datastore/v2/feedbacks'
48
+
49
+ end
50
+ # raises: Clashing endpoints.
28
51
  ```
29
52
 
30
- [Read more about data](docs/data.md)
53
+ ## Find multiple records
54
+
55
+ You can query a backend service to provide records by using `where`.
56
+
57
+ ```ruby
58
+ Feedback.where(has_reviews: true)
59
+ ```
60
+
61
+ This uses the `:datastore/v2/feedbacks` endpoint, cause `:campaign_id` was not provided. In addition it would add `?has_reviews=true` to the get parameters.
62
+
63
+ ```ruby
64
+ Feedback.where(campaign_id: 'fq-a81ngsl1d')
65
+ ```
66
+
67
+ Uses the `:datastore/v2/content-ads/:campaign_id/feedbacks` endpoint.
68
+
69
+ ## Find single records
70
+
71
+ `find` finds a unique record by uniqe identifier (usualy id).
72
+
73
+ If no record is found an error is raised.
31
74
 
32
75
  ## Proxy
33
- A proxy is used to access data. It is divided in Collection and Item.
76
+ Instead of mapping data when it arrives from the backend, the proxy makes data accessible when you access it, not when you fetch it. The proxy is used to access data and it is divided in `Collection` and `Item`.
77
+
78
+ `find` can also be used to find a single uniqe record with parameters:
79
+
80
+ ```ruby
81
+ Feedback.find(campaign_id: 123, id: 456)
82
+ ```
83
+
84
+ `find_by` finds the first record matching the specified conditions.
85
+
86
+ If no record is found, `nil` is returned.
87
+
88
+ `find_by!` raises LHC::NotFound if nothing was found.
89
+
90
+ ```ruby
91
+ Feedback.find_by(id: 'z12f-3asm3ngals')
92
+ Feedback.find_by(id: 'doesntexist') # nil
93
+ ```
94
+
95
+ `first` is an alias for finding the first record without parameters.
96
+
97
+ ```ruby
98
+ Feedback.first
99
+ ```
100
+
101
+ If no record is found, `nil` is returned.
102
+
103
+ `first!` raises LHC::NotFound if nothing was found.
104
+
105
+ ## Batch processing
34
106
 
35
- For every proxy that contains an `href` you can use `load!` or `reload!` to receive latest backend data.
107
+ **Be carefull using methods for batch processing. They could result in a lot of HTTP requests!**
108
+
109
+ `all` fetches all records from the backend by doing multiple requests if necessary.
36
110
 
37
111
  ```ruby
38
- {
39
- "href" => "http://local.ch/v2/content-ads/51dfc5690cf271c375c5a12d"
40
- }
112
+ data = Feedback.all
113
+ data.count # 998
114
+ data.total # 998
115
+ ```
116
+
117
+ `find_each` is a more fine grained way to process single records that are fetched in batches.
41
118
 
42
- item.load!.id
43
- item.reload! # loads it again
44
- item.load! # wont load it again, because its already arround
119
+ ```ruby
120
+ Feedback.find_each(start: 50, batch_size: 20, params: { has_reviews: true }) do |feedback|
121
+ # Iterates over each record. Starts with record nr. 50 and fetches 20 records each batch.
122
+ feedback
123
+ break if feedback.some_attribute == some_value
124
+ end
125
+ ```
126
+
127
+ `find_in_batches` is used by `find_each` and processes batches.
128
+ ```ruby
129
+ Feedback.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }) do |feedbacks|
130
+ # Iterates over multiple records (batch size is 20). Starts with record nr. 50 and fetches 20 records each batch.
131
+ feedbacks
132
+ break if feedback.some_attribute == some_value
133
+ end
45
134
  ```
46
135
 
47
- ## Collection
48
- A collection contains multiple items.
136
+ ## Create records
49
137
 
50
138
  ```ruby
51
- data = Feedback.where(has_reviews: true) #<LHS::Data @_proxy_=#<LHS::Collection>>
52
- data.count # 10
53
- data.total # 98
139
+ feedback = Feedback.create(
140
+ recommended: true,
141
+ source_id: 'aaa',
142
+ content_ad_id: '1z-5r1fkaj'
143
+ )
144
+ ```
145
+
146
+ When creation fails, the object contains errors. It provides them through the `errors` attribute:
147
+
148
+ ```ruby
149
+ feedback.errors #<LHS::Errors>
150
+ feedback.errors.include?(:ratings) # true
151
+ feedback.errors[:ratings] # ['REQUIRED_PROPERTY_VALUE']
152
+ record.errors.messages # {:ratings=>["REQUIRED_PROPERTY_VALUE"], :recommended=>["REQUIRED_PROPERTY_VALUE"]}
153
+ record.errors.message # ratings must be set when review or name or review_title is set | The property value is required; it cannot be null, empty, or blank."
154
+ ```
155
+
156
+ ## Build new records
157
+
158
+ Build and persist new items from scratch are done either with `new` or it's alias `build`.
159
+
160
+ ```ruby
161
+ feedback = Feedback.new(recommended: true)
162
+ feedback.save
163
+ ```
164
+
165
+ ## Include linked resources
166
+
167
+ When fetching records, you can specify in advance all the linked resources that you want to include in the results. With `includes`, LHS ensures that all matching and explicitly linked resources are loaded and merged.
168
+
169
+ The implementation is heavily influenced by [http://guides.rubyonrails.org/active_record_class_querying](http://guides.rubyonrails.org/active_record_class_querying.html#eager-loading-associations) and you should read it to understand this feature in all its glory.
170
+
171
+ ### One-Level `includes`
172
+
173
+ ```ruby
174
+ # a claim has a localch_account
175
+ claims = Claims.includes(:localch_account).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
176
+ claims.first.localch_account.email # 'test@email.com'
177
+ ```
178
+ * [see the JSON without include](examples/claim_no_include.json)
179
+ * [see the JSON with include](examples/claim_with_include.json)
180
+
181
+ ### Two-Level `includes`
182
+
183
+ ```ruby
184
+ # a feedback has a campaign, which has an entry
185
+ feedbacks = Feedback.includes(campaign: :entry).where(has_reviews: true)
186
+ feedbacks.first.campaign.entry.name # 'Casa Ferlin'
187
+ ```
188
+
189
+ ### Multiple `includes`
190
+
191
+ ```ruby
192
+ # list of includes
193
+ claims = Claims.includes(:localch_account, :entry).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
194
+
195
+ # array of includes
196
+ claims = Claims.includes([:localch_account, :entry]).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
197
+
198
+ # Two-level with array of includes
199
+ feedbacks = Feedback.includes(campaign: [:entry, :user]).where(has_reviews: true)
200
+ ```
201
+
202
+ ### Known LHS::Records are used to request linked resources
203
+
204
+ When including linked resources with `includes`, known/defined services and endpoints are used to make those requests.
205
+ That also means that options for endpoints of linked resources are applied when requesting those in addition.
206
+ This allows you to include protected resources (e.g. OAuth) as endpoint options for oauth authentication get applied.
207
+
208
+ The [Auth Inteceptor](https://github.com/local-ch/lhc-core-interceptors#auth-interceptor) from [lhc-core-interceptors](https://github.com/local-ch/lhc-core-interceptors) is used to configure the following endpoints.
209
+
210
+ ```ruby
211
+ class Favorite < LHS::Record
212
+
213
+ endpoint ':datastore/:user_id/favorites', auth: { bearer: -> { bearer_token } }
214
+ endpoint ':datastore/:user_id/favorites/:id', auth: { bearer: -> { bearer_token } }
215
+
216
+ end
217
+
218
+ class Place < LHS::Record
219
+
220
+ endpoint ':datastore/v2/places', auth: { bearer: -> { bearer_token } }
221
+ endpoint ':datastore/v2/places/:id', auth: { bearer: -> { bearer_token } }
222
+
223
+ end
224
+
225
+ Favorite.includes(:place).where(user_id: current_user.id)
226
+ # Will include places and applies endpoint options to authenticate the request.
227
+ ```
228
+
229
+ ## Map data
230
+
231
+ To influence how data is accessed/provied, you can use mappings to either map deep nested data or to manipulate data when its accessed. Simply create methods inside the LHS::Record. They can access underlying data:
232
+
233
+ ```ruby
234
+ class LocalEntry < LHS::Record
235
+ endpoint ':datastore/v2/local-entries'
236
+
237
+ def name
238
+ addresses.first.business.identities.first.name
239
+ end
240
+
241
+ end
242
+ ```
243
+
244
+ ### Known LHS::Records when accessing mapped data from nested data
245
+
246
+ As LHS detects LHS::Records as soon as a link is present, mappings will also be applied on nested data:
247
+
248
+ ```
249
+ class Place < LHS::Record
250
+ endpoint ':datastore/v2/places'
251
+
252
+ def name
253
+ addresses.first.business.identities.first.name
254
+ end
255
+ end
256
+
257
+ class Favorite < LHS::Record
258
+ endpoint ':datastore/v2/favorites'
259
+ end
260
+
261
+ favorite = Favorite.includes(:place).find(1)
262
+ favorite.place.name # local.ch AG
263
+ ```
264
+
265
+ ## Setters
266
+
267
+ You can change attributes of LHS::Records:
268
+
269
+ ```
270
+ record = Feedback.find(id: 'z12f-3asm3ngals')
271
+ rcord.recommended = false
272
+ ```
273
+
274
+ ## Save
275
+
276
+ You can persist changes with `save`. `save` will return `false` if persisting fails. `save!` instead will raise an exception.
277
+
278
+ ```ruby
279
+ feedback = Feedback.find('1z-5r1fkaj')
280
+ feedback.recommended = false
281
+ feedback.save
282
+ ```
283
+
284
+ ## Update
285
+
286
+ `update` will return false if persisting fails. `update!` instead will an raise exception.
287
+
288
+ `update` always updates the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is updated.
289
+
290
+ ```ruby
291
+ feedback = Feedback.find('1z-5r1fkaj')
292
+ feedback.update(recommended: false)
293
+ ```
294
+
295
+ ## Destroy
296
+
297
+ You can delete records remotely by calling `destroy` on an LHS::Record.
298
+
299
+ ```ruby
300
+ feedback = Feedback.find('1z-5r1fkaj')
301
+ feedback.destroy
302
+ ```
303
+
304
+ ## Validation
305
+
306
+ In order to validate LHS::Records before persisting them, you can use the `valid?` (`validate` alias) method.
307
+
308
+ The specific endpoint has to support validations with the `persist=false` parameter. The endpoint has to be enabled (opt-in) for validations in the service configuration.
309
+
310
+ ```
311
+ class User < LHS::Record
312
+ endpoint ':datastore/v2/users', validates: true
313
+ end
314
+
315
+ user = User.build(email: 'im not an email address')
316
+ unless user.valid?
317
+ fail(user.errors[:email])
318
+ end
54
319
  ```
55
320
 
56
- [Read more about collections](docs/collections.md)
321
+ ## Collections: Offset / Limit / Pagination
57
322
 
58
- ## Item
59
- An item is a concrete record. It can be part of another proxy like collection.
323
+ You can paginate by passing offset, and limit params. They will be forwarded to the backend.
60
324
 
61
325
  ```ruby
62
- data = Feedback.where(has_reviews: true).first #<LHS::Data @_proxy_=#<LHS::Item>>
63
- data.recommended # true
64
- data.created_date # Fri, 19 Sep 2014 14:03:35 +0200
326
+ data = Feedback.where(limit: 50)
327
+ data.count // 50
328
+ Feedback.where(limit: 50, offset: 51)
65
329
  ```
66
330
 
67
- [Read more about items](docs/items.md)
331
+ `total` provides total amount of items (even if paginated).
332
+ `limit` provides amount of items per page.
333
+ `offset` provides how many items where skipped to start the current page.
@@ -7,11 +7,11 @@ require "lhs/version"
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "lhs"
9
9
  s.version = LHS::VERSION
10
- s.authors = ['local.ch']
10
+ s.authors = ['https://github.com/local-ch/lhs/graphs/contributors']
11
11
  s.email = ['ws-operations@local.ch']
12
12
  s.homepage = 'https://github.com/local-ch/lhs'
13
- s.summary = 'LocalHttpServices'
14
- s.description = 'Rails gem providing an easy interface to use http services here at local'
13
+ s.summary = 'Rails gem providing an easy, active-record-like interface to use http backend services'
14
+ s.description = s.summary
15
15
 
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- spec/*`.split("\n")
data/lib/lhs.rb CHANGED
@@ -4,10 +4,3 @@ module LHS
4
4
  end
5
5
 
6
6
  Gem.find_files('lhs/**/*.rb').each { |path| require path }
7
-
8
- # Preload all the services that are defined in app/services
9
- class Engine < Rails::Engine
10
- initializer 'Load all services' do |app|
11
- Dir.glob(app.root.join('app/services/**/*.rb')).each {|file| require file }
12
- end
13
- end
@@ -25,7 +25,7 @@ class LHS::Collection < LHS::Proxy
25
25
  def _collection
26
26
  raw = _data._raw if _data._raw.is_a?(Array)
27
27
  raw ||= _data._raw[:items]
28
- Collection.new(raw, _data, _data._service)
28
+ Collection.new(raw, _data, _data._record_class)
29
29
  end
30
30
 
31
31
  def _raw
@@ -59,16 +59,16 @@ class LHS::Collection < LHS::Proxy
59
59
  attr_accessor :raw
60
60
  delegate :last, :sample, :[], :present?, :blank?, :empty?, to: :raw
61
61
 
62
- def initialize(raw, parent, service)
62
+ def initialize(raw, parent, record)
63
63
  self.raw = raw
64
64
  @parent = parent
65
- @service = service
65
+ @record = record
66
66
  end
67
67
 
68
68
  def each(&block)
69
69
  raw.each do |item|
70
70
  if item.is_a? Hash
71
- yield LHS::Data.new(item, @parent, @service)
71
+ yield LHS::Data.new(item, @parent, @record)
72
72
  else
73
73
  yield item
74
74
  end
@@ -7,8 +7,8 @@ class LHS::Item < LHS::Proxy
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  def destroy
10
- service = _data._root._service
11
- _data._request = service.request(method: :delete, url: href)._request
10
+ record = _data._root._record_class
11
+ _data._request = record.request(method: :delete, url: href)._request
12
12
  _data
13
13
  end
14
14
  end
@@ -14,14 +14,14 @@ class LHS::Item < LHS::Proxy
14
14
  end
15
15
 
16
16
  def save!
17
- service = _data._root._service
17
+ record = _data._root._record_class
18
18
  data = _data._raw.dup
19
19
  url = if href.present?
20
20
  href
21
21
  else
22
- service.find_endpoint(data).compile(data)
22
+ record.find_endpoint(data).compile(data)
23
23
  end
24
- data = service.request(method: :post, url: url, body: data.to_json, headers: {'Content-Type' => 'application/json'})
24
+ data = record.request(method: :post, url: url, body: data.to_json, headers: {'Content-Type' => 'application/json'})
25
25
  self._data.merge_raw!(data)
26
26
  true
27
27
  end