lhs 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.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +38 -0
  3. data/Gemfile +11 -0
  4. data/README.md +67 -0
  5. data/Rakefile +26 -0
  6. data/docs/collections.md +28 -0
  7. data/docs/data.md +39 -0
  8. data/docs/examples/claim_no_include.json +16 -0
  9. data/docs/examples/claim_with_include.json +47 -0
  10. data/docs/items.md +55 -0
  11. data/docs/service.jpg +0 -0
  12. data/docs/service.pdf +649 -3
  13. data/docs/services.md +191 -0
  14. data/lhs.gemspec +31 -0
  15. data/lib/lhs.rb +6 -0
  16. data/lib/lhs/collection.rb +78 -0
  17. data/lib/lhs/concerns/data/json.rb +12 -0
  18. data/lib/lhs/concerns/item/destroy.rb +15 -0
  19. data/lib/lhs/concerns/item/save.rb +29 -0
  20. data/lib/lhs/concerns/item/update.rb +24 -0
  21. data/lib/lhs/concerns/service/all.rb +24 -0
  22. data/lib/lhs/concerns/service/batch.rb +37 -0
  23. data/lib/lhs/concerns/service/build.rb +17 -0
  24. data/lib/lhs/concerns/service/create.rb +26 -0
  25. data/lib/lhs/concerns/service/endpoints.rb +82 -0
  26. data/lib/lhs/concerns/service/find.rb +36 -0
  27. data/lib/lhs/concerns/service/find_by.rb +35 -0
  28. data/lib/lhs/concerns/service/first.rb +19 -0
  29. data/lib/lhs/concerns/service/includes.rb +21 -0
  30. data/lib/lhs/concerns/service/mapping.rb +23 -0
  31. data/lib/lhs/concerns/service/model.rb +16 -0
  32. data/lib/lhs/concerns/service/request.rb +96 -0
  33. data/lib/lhs/concerns/service/where.rb +16 -0
  34. data/lib/lhs/data.rb +103 -0
  35. data/lib/lhs/errors.rb +86 -0
  36. data/lib/lhs/item.rb +83 -0
  37. data/lib/lhs/proxy.rb +26 -0
  38. data/lib/lhs/service.rb +20 -0
  39. data/lib/lhs/version.rb +3 -0
  40. data/script/ci/build.sh +19 -0
  41. data/spec/collection/meta_data_spec.rb +54 -0
  42. data/spec/collection/respond_to_spec.rb +19 -0
  43. data/spec/collection/without_object_items_spec.rb +26 -0
  44. data/spec/data/collection_spec.rb +36 -0
  45. data/spec/data/item_spec.rb +44 -0
  46. data/spec/data/merge_spec.rb +32 -0
  47. data/spec/data/raw_spec.rb +39 -0
  48. data/spec/data/respond_to_spec.rb +26 -0
  49. data/spec/data/root_spec.rb +25 -0
  50. data/spec/data/to_json_spec.rb +39 -0
  51. data/spec/dummy/README.rdoc +28 -0
  52. data/spec/dummy/Rakefile +6 -0
  53. data/spec/dummy/app/assets/images/.keep +0 -0
  54. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  55. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  57. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  58. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  59. data/spec/dummy/app/mailers/.keep +0 -0
  60. data/spec/dummy/app/models/.keep +0 -0
  61. data/spec/dummy/app/models/concerns/.keep +0 -0
  62. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  63. data/spec/dummy/bin/bundle +3 -0
  64. data/spec/dummy/bin/rails +4 -0
  65. data/spec/dummy/bin/rake +4 -0
  66. data/spec/dummy/config.ru +4 -0
  67. data/spec/dummy/config/application.rb +14 -0
  68. data/spec/dummy/config/boot.rb +5 -0
  69. data/spec/dummy/config/environment.rb +5 -0
  70. data/spec/dummy/config/environments/development.rb +34 -0
  71. data/spec/dummy/config/environments/production.rb +75 -0
  72. data/spec/dummy/config/environments/test.rb +39 -0
  73. data/spec/dummy/config/initializers/assets.rb +8 -0
  74. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  75. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  77. data/spec/dummy/config/initializers/inflections.rb +16 -0
  78. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  79. data/spec/dummy/config/initializers/session_store.rb +3 -0
  80. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  81. data/spec/dummy/config/locales/en.yml +23 -0
  82. data/spec/dummy/config/routes.rb +56 -0
  83. data/spec/dummy/config/secrets.yml +22 -0
  84. data/spec/dummy/lib/assets/.keep +0 -0
  85. data/spec/dummy/public/404.html +67 -0
  86. data/spec/dummy/public/422.html +67 -0
  87. data/spec/dummy/public/500.html +66 -0
  88. data/spec/dummy/public/favicon.ico +0 -0
  89. data/spec/item/destroy_spec.rb +39 -0
  90. data/spec/item/getter_spec.rb +24 -0
  91. data/spec/item/respond_to_spec.rb +29 -0
  92. data/spec/item/save_errors_spec.rb +48 -0
  93. data/spec/item/save_spec.rb +58 -0
  94. data/spec/item/setter_spec.rb +38 -0
  95. data/spec/item/update_spec.rb +56 -0
  96. data/spec/proxy/load_spec.rb +47 -0
  97. data/spec/rails_helper.rb +9 -0
  98. data/spec/service/all_spec.rb +31 -0
  99. data/spec/service/build_spec.rb +25 -0
  100. data/spec/service/create_spec.rb +81 -0
  101. data/spec/service/creation_failed_spec.rb +54 -0
  102. data/spec/service/endpoint_misconfiguration_spec.rb +26 -0
  103. data/spec/service/endpoint_options_spec.rb +23 -0
  104. data/spec/service/endpoints_spec.rb +57 -0
  105. data/spec/service/find_by_spec.rb +49 -0
  106. data/spec/service/find_each_spec.rb +47 -0
  107. data/spec/service/find_in_batches_spec.rb +68 -0
  108. data/spec/service/find_spec.rb +71 -0
  109. data/spec/service/first_spec.rb +39 -0
  110. data/spec/service/includes_spec.rb +61 -0
  111. data/spec/service/mapping_spec.rb +72 -0
  112. data/spec/service/model_name_spec.rb +17 -0
  113. data/spec/service/request_spec.rb +22 -0
  114. data/spec/service/where_spec.rb +33 -0
  115. data/spec/spec_helper.rb +4 -0
  116. data/spec/support/cleanup_configuration.rb +17 -0
  117. data/spec/support/cleanup_services.rb +20 -0
  118. data/spec/support/fixtures/json/feedback.json +11 -0
  119. data/spec/support/fixtures/json/feedbacks.json +174 -0
  120. data/spec/support/fixtures/json/localina_content_ad.json +23 -0
  121. data/spec/support/load_json.rb +3 -0
  122. metadata +346 -0
@@ -0,0 +1,191 @@
1
+ Services
2
+ ===
3
+
4
+ A LHS::Service makes data available using multiple endpoints.
5
+
6
+ ![Service](service.jpg)
7
+
8
+ ## Endpoints
9
+
10
+ You setup a service by configure one or multiple backend endpoints.
11
+ You can also add request options for an endpoint (see following example).
12
+
13
+ ```ruby
14
+ class Feedback < LHS::Service
15
+
16
+ endpoint ':datastore/v2/content-ads/:campaign_id/feedbacks'
17
+ endpoint ':datastore/v2/feedbacks', cache: true, cache_expires_in: 1.day
18
+
19
+ end
20
+ ```
21
+
22
+ If you try to setup a service with clashing endpoints it will immediately raise an exception.
23
+
24
+ ```ruby
25
+ class Feedback < LHS::Service
26
+
27
+ endpoint ':datastore/v2/reviews'
28
+ endpoint ':datastore/v2/feedbacks'
29
+
30
+ end
31
+ # raises: Clashing endpoints.
32
+
33
+ ```
34
+
35
+ ## Find multiple records
36
+
37
+ You can query the services by using `where`.
38
+
39
+ ```ruby
40
+ Feedback.where(has_reviews: true) #<LHS::Data @_proxy=#<LHS::Collection>>
41
+ ```
42
+
43
+ This uses the `:datastore/v2/feedbacks` endpoint, cause `:campaign_id` was not provided.
44
+ In addition it would add `?has_reviews=true` to the get parameters.
45
+
46
+ ```ruby
47
+ Feedback.where(campaign_id: 'fq-a81ngsl1d') #<LHS::Data @_proxy=#<LHS::Collection>>
48
+ ```
49
+ Uses the `:datastore/v2/content-ads/:campaign_id/feedbacks` endpoint.
50
+
51
+ → [Read more about collections](collections.md)
52
+
53
+ ## Find single records
54
+
55
+ `find` finds a unique item by uniqe identifier (usualy id).
56
+
57
+ If no record is found an error is raised.
58
+
59
+ ```ruby
60
+ Feedback.find('z12f-3asm3ngals') #<LHS::Data @_proxy=#<LHS::Item>>
61
+ ```
62
+
63
+ `find` can also be used to find a single uniqe item with parameters:
64
+
65
+ ```ruby
66
+ Feedback.find(campaign_id: 123, id: 456)
67
+ ```
68
+
69
+ `find_by` finds the first record matching the specified conditions.
70
+
71
+ If no record is found, `nil` is returned.
72
+
73
+ `find_by!` raises LHC::NotFound if nothing was found.
74
+
75
+ ```ruby
76
+ Feedback.find_by(id: 'z12f-3asm3ngals') #<LHS::Data @_proxy=#<LHS::Item>>
77
+ Feedback.find_by(id: 'doesntexist') # nil
78
+ ```
79
+
80
+ `first` is a alias for finding the first of a service without parameters.
81
+
82
+ ```ruby
83
+ Feedback.first
84
+ ```
85
+
86
+ If no record is found, `nil` is returned.
87
+
88
+ `first!` raises LHC::NotFound if nothing was found.
89
+
90
+ → [Read more about items](items.md)
91
+
92
+ ## Batch processing
93
+
94
+ ** Be carefull using methods for batch processing. They could result in a lot of HTTP requests! **
95
+
96
+ `all` fetches all records from the backend by doing multiple requests if necessary.
97
+
98
+ ```ruby
99
+ data = Feedback.all #<LHS::Data @_proxy=#<LHS::Collection>>
100
+ data.count # 998
101
+ data.total # 998
102
+ ```
103
+
104
+ → [Read more about collections](collections.md)
105
+
106
+ `find_each` is a more fine grained way to process single records that are fetched in batches.
107
+
108
+ ```ruby
109
+ Feedback.find_each(start: 50, batch_size: 20, params: { has_reviews: true }) do |feedback|
110
+ # Iterates over each record. Starts with record nr. 50 and fetches 20 records each batch.
111
+ feedback #<LHS::Data @_proxy=#<LHS::Item>>
112
+ end
113
+ ```
114
+
115
+ `find_in_batches` is used by `find_each` and processes batches.
116
+ ```ruby
117
+ Feedback.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }) do |feedbacks|
118
+ # Iterates over multiple records (batch size is 20). Starts with record nr. 50 and fetches 20 records each batch.
119
+ feedbacks #<LHS::Data @_proxy=#<LHS::Collection>>
120
+ end
121
+ ```
122
+
123
+ ## Create records
124
+
125
+ ```ruby
126
+ feedback = Feedback.create(
127
+ recommended: true,
128
+ source_id: 'aaa',
129
+ content_ad_id: '1z-5r1fkaj'
130
+ ) #<LHS::Data @_proxy=#<LHS::Item>>
131
+ ```
132
+
133
+ When creation fails, the object contains errors in its `errors` attribute:
134
+
135
+ ```ruby
136
+ feedback.errors #<LHS::Errors>
137
+ feedback.errors.include?(:ratings) # true
138
+ feedback.errors[:ratings] # ['REQUIRED_PROPERTY_VALUE']
139
+ record.errors.messages # {:ratings=>["REQUIRED_PROPERTY_VALUE"], :recommended=>["REQUIRED_PROPERTY_VALUE"]}
140
+ 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."
141
+ ```
142
+
143
+ ## Build new records
144
+
145
+ Build and persist new items from scratch.
146
+
147
+ ```ruby
148
+ feedback = Feedback.build(recommended: true)
149
+ feedback.save
150
+ ```
151
+
152
+ → [Read more about items](items.md)
153
+
154
+
155
+ ## Include linked resources
156
+
157
+ A service lets you specify in advance all the linked resources that you want to include in the results. With includes, a service ensures that all matching and explicitly linked resources are loaded and merged.
158
+
159
+ The implementation is heavily influenced by [http://guides.rubyonrails.org/active_record_querying](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
160
+ and you should read it to understand this feature in all its glory.
161
+
162
+ ### One-Level `includes`
163
+
164
+ ```ruby
165
+ # a claim has a localch_account
166
+ claims = Claims.includes(:localch_account).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
167
+ claims.first.localch_account.email # 'test@email.com'
168
+ ```
169
+ * [see the JSON without include](examples/claim_no_include.json)
170
+ * [see the JSON with include](examples/claim_with_include.json)
171
+
172
+ ### Two-Level `includes`
173
+
174
+ ```ruby
175
+ # a feedback has a campaign, which has an entry
176
+ feedbacks = Feedback.includes(campaign: :entry).where(has_reviews: true)
177
+ feedbacks.first.campaign.entry.name # 'Casa Ferlin'
178
+ ```
179
+
180
+ ## Map data
181
+
182
+ To influence how data is accessed/provied, you can use mapping to either map deep nested data or to manipulate data when its accessed:
183
+
184
+ ```ruby
185
+ class LocalEntry < LHS::Service
186
+ endpoint ':datastore/v2/local-entries'
187
+
188
+ map :name, ->(entry){ entry.addresses.first.business.identities.first.name }
189
+
190
+ end
191
+ ```
@@ -0,0 +1,31 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require "lhs/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = "lhs"
9
+ s.version = LHS::VERSION
10
+ s.authors = ['local.ch']
11
+ s.email = ['ws-operations@local.ch']
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'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- spec/*`.split("\n")
18
+ s.require_paths = ['lib']
19
+
20
+ s.requirements << 'Ruby >= 1.9.2'
21
+ s.required_ruby_version = '>= 1.9.2'
22
+
23
+ s.add_dependency 'lhc', '>= 0.2.0'
24
+ s.add_dependency 'lhc-core-interceptors', '>= 0.1.0'
25
+
26
+ s.add_development_dependency 'rspec-rails', '>= 3.0.0'
27
+ s.add_development_dependency 'rails', '>= 4.0.0'
28
+ s.add_development_dependency 'webmock'
29
+ s.add_development_dependency 'geminabox'
30
+ s.add_development_dependency 'pry'
31
+ end
@@ -0,0 +1,6 @@
1
+ require 'lhc'
2
+
3
+ module LHS
4
+ end
5
+
6
+ Gem.find_files('lhs/**/*.rb').each { |path| require path }
@@ -0,0 +1,78 @@
1
+ require File.join(__dir__, 'proxy.rb')
2
+
3
+ # A collection is a special type of data
4
+ # that contains multiple items
5
+ class LHS::Collection < LHS::Proxy
6
+
7
+ def total
8
+ _data._raw['total']
9
+ end
10
+
11
+ def limit
12
+ _data._raw['limit']
13
+ end
14
+
15
+ def offset
16
+ _data._raw['offset']
17
+ end
18
+
19
+ def href
20
+ _data._raw['href']
21
+ end
22
+
23
+ def _collection
24
+ raw = _data._raw if _data._raw.is_a?(Array)
25
+ raw ||= _data._raw['items']
26
+ Collection.new(raw, _data, _data._service)
27
+ end
28
+
29
+ def _raw
30
+ _data._raw
31
+ end
32
+
33
+ protected
34
+
35
+ def method_missing(name, *args, &block)
36
+ value = _collection.send(name, *args, &block)
37
+ if value.is_a? Hash
38
+ data = LHS::Data.new(value, _data)
39
+ item = LHS::Item.new(data)
40
+ LHS::Data.new(item, _data)
41
+ else
42
+ value
43
+ end
44
+ end
45
+
46
+ def respond_to_missing?(name, include_all = false)
47
+ _collection.respond_to?(name, include_all)
48
+ end
49
+
50
+ private
51
+
52
+ # The internal collection class that includes enumerable
53
+ # and insures to return LHS::Items in case of iterating items
54
+ class Collection
55
+ include Enumerable
56
+
57
+ attr_accessor :raw
58
+
59
+ def initialize(raw, parent, service)
60
+ self.raw = raw
61
+ @parent = parent
62
+ @service = service
63
+ end
64
+
65
+ def each(&block)
66
+ raw.each do |item|
67
+ if item.is_a? Hash
68
+ yield LHS::Data.new(item, @parent, @service)
69
+ else
70
+ yield item
71
+ end
72
+ end
73
+ end
74
+
75
+ delegate :sample, to: :raw
76
+ delegate :[], to: :raw
77
+ end
78
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support'
2
+
3
+ class LHS::Data
4
+
5
+ module Json
6
+ extend ActiveSupport::Concern
7
+
8
+ def as_json(options = {})
9
+ _data._raw.as_json
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support'
2
+ require File.dirname(__FILE__) + '/../../proxy'
3
+
4
+ class LHS::Item < LHS::Proxy
5
+
6
+ module Destroy
7
+ extend ActiveSupport::Concern
8
+
9
+ def destroy
10
+ service_instance = _data._root._service.instance
11
+ _data._request = service_instance.request(method: :delete, url: href)._request
12
+ _data
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ require 'active_support'
2
+ require File.dirname(__FILE__) + '/../../proxy'
3
+
4
+ class LHS::Item < LHS::Proxy
5
+
6
+ module Save
7
+ extend ActiveSupport::Concern
8
+
9
+ def save
10
+ save!
11
+ rescue LHC::Error => e
12
+ self.errors = LHS::Errors.new(e.response)
13
+ false
14
+ end
15
+
16
+ def save!
17
+ service = _data._root._service
18
+ data = _data._raw.dup
19
+ url = if href.present?
20
+ href
21
+ else
22
+ service.instance.find_endpoint(data).compile(data)
23
+ end
24
+ response = service.instance.request(method: :post, url: url, body: data.to_json, headers: {'Content-Type' => 'application/json'})
25
+ self._data.merge!(response)
26
+ true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support'
2
+ require File.dirname(__FILE__) + '/../../proxy'
3
+
4
+ class LHS::Item < LHS::Proxy
5
+
6
+ module Update
7
+ extend ActiveSupport::Concern
8
+
9
+ def update(params)
10
+ update!(params)
11
+ rescue LHC::Error => e
12
+ self.errors = LHS::Errors.new(e.response)
13
+ false
14
+ end
15
+
16
+ def update!(params)
17
+ service = _data._root._service
18
+ data = _data._raw.dup
19
+ response = service.instance.request(method: :post, url: href, body: data.merge(params).to_json, headers: {'Content-Type' => 'application/json'})
20
+ self._data.merge!(response)
21
+ true
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support'
2
+
3
+ class LHS::Service
4
+
5
+ module All
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ def all(params = {})
11
+ all = []
12
+ data = instance.request(params: params.merge(limit: 100))
13
+ all.concat(data._raw['items'])
14
+ total_left = data._raw['total'] - data.count
15
+ requests = total_left / data._raw['limit']
16
+ requests.times do |i|
17
+ offset = data._raw['limit'] * (i+1) + 1
18
+ all.concat instance.request(params: params.merge(limit: data._raw['limit'], offset: offset))._raw['items']
19
+ end
20
+ LHS::Data.new(all, nil, self)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_support'
2
+
3
+ class LHS::Service
4
+
5
+ module Batch
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # Process single entries fetched in batches
11
+ def find_each(options = {})
12
+ find_in_batches(options) do |data|
13
+ data.each do |record|
14
+ item = LHS::Item.new(LHS::Data.new(record, data, self.class))
15
+ yield LHS::Data.new(item, data, self.class)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Process batches of entries
21
+ def find_in_batches(options = {})
22
+ fail 'No block given' unless block_given?
23
+ start = options[:start] || 1
24
+ batch_size = options[:batch_size] || 100
25
+ params = options[:params] || {}
26
+ loop do # as suggested by Matz
27
+ data = instance.request(params: params.merge(limit: batch_size, offset: start))
28
+ batch_size = data._raw['limit']
29
+ left = data._raw['total'].to_i - data._raw['offset'].to_i - data._raw['limit'].to_i
30
+ yield data
31
+ break if left <= 0
32
+ start += batch_size
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end