lhs 0.3.0 → 0.4.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: 143dbe83c79a073cfcb8af80d7a17792fefe5258
4
- data.tar.gz: 96dd3c2a2fecd33be80846cfca58d05386df3b42
3
+ metadata.gz: 0ff25a42028f674f80afd08d6a152b42bf61b08b
4
+ data.tar.gz: c123d197b870914fcb564ba2ce152293105c81c9
5
5
  SHA512:
6
- metadata.gz: 6737f3af323f812f43158b378fede1b1a034aa1c831c7a01ee5bb66a424f03b549f3c5ed7f1547edc93f4c9f38257cf3e9ad30e31dbf15d65720e03939f05cc3
7
- data.tar.gz: 9b77bd37281a5c85be48b9fdea43e80e4273a1959cb8ef7935f118b8c4f98bc4c223559a266a33fef398c3cfa3d1fed4de05f6f41561dfa56fdcad9ee307a229
6
+ metadata.gz: 333cf3687bde474f71b5fcda3ffae1b375ce565fd9c7341888d5fd810d07ae51a137b1b34eb0b31b6d40f5e5d16b94e2330020327df605f3771f3a35224f2bbc
7
+ data.tar.gz: b43d5f519fa79472617485bbfe21bdc757d97b38ba49b17d2e1d485562f5821e4968d66dd371a3ea5d81a67ae6d071d9610d3d0f14053ff37c378459596918bf
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.intra.local.ch/'
1
+ source 'https://rubygems.org/'
2
2
 
3
3
  # Declare your gem's dependencies in lhs.gemspec.
4
4
  # Bundler will treat runtime dependencies like base dependencies, and
@@ -8,4 +8,4 @@ gemspec
8
8
  # Declare any dependencies that are still in development here instead of in
9
9
  # your gemspec. These might include edge Rails or gems from your path or
10
10
  # Git. Remember to move these dependencies to your gemspec before releasing
11
- # your gem to rubygems.org.
11
+ # your gem to rubygems.org.
@@ -5,6 +5,10 @@ A LHS::Service makes data available using multiple endpoints.
5
5
 
6
6
  ![Service](service.jpg)
7
7
 
8
+ ## Convention
9
+
10
+ Please store all defined services in `app/services` as they are autoloaded from this directory.
11
+
8
12
  ## Endpoints
9
13
 
10
14
  You setup a service by configure one or multiple backend endpoints.
@@ -177,6 +181,32 @@ and you should read it to understand this feature in all its glory.
177
181
  feedbacks.first.campaign.entry.name # 'Casa Ferlin'
178
182
  ```
179
183
 
184
+ ### Known services are used to request linked resources
185
+
186
+ When including linked resources with `includes`, known/defined services and endpoints are used to make those requests.
187
+ That also means that options for endpoints of linked resources are applied when requesting those in addition.
188
+ This enables you to include protected resources (e.g. OAuth) as endpoint options for oauth authentication get applied.
189
+
190
+ 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.
191
+ ```ruby
192
+ class Favorite < LHS::Service
193
+
194
+ endpoint ':datastore/:user_id/favorites', auth: { bearer: -> { bearer_token } }
195
+ endpoint ':datastore/:user_id/favorites/:id', auth: { bearer: -> { bearer_token } }
196
+
197
+ end
198
+
199
+ class Place < LHS::Service
200
+
201
+ endpoint ':datastore/v2/places', auth: { bearer: -> { bearer_token } }
202
+ endpoint ':datastore/v2/places/:id', auth: { bearer: -> { bearer_token } }
203
+
204
+ end
205
+
206
+ Favorite.includes(:place).where(user_id: current_user.id)
207
+ # Will include places and applies endpoint options to authenticate the request.
208
+ ```
209
+
180
210
  ## Map data
181
211
 
182
212
  To influence how data is accessed/provied, you can use mapping to either map deep nested data or to manipulate data when its accessed:
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.requirements << 'Ruby >= 1.9.2'
21
21
  s.required_ruby_version = '>= 1.9.2'
22
22
 
23
- s.add_dependency 'lhc', '>= 0.2.0'
23
+ s.add_dependency 'lhc', '>= 1.2.0'
24
24
  s.add_dependency 'lhc-core-interceptors', '>= 0.1.0'
25
25
 
26
26
  s.add_development_dependency 'rspec-rails', '>= 3.0.0'
data/lib/lhs.rb CHANGED
@@ -4,3 +4,10 @@ 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
@@ -10,6 +10,7 @@ class LHS::Service
10
10
  extend ActiveSupport::Concern
11
11
 
12
12
  attr_accessor :endpoints
13
+ mattr_accessor :all
13
14
 
14
15
  module ClassMethods
15
16
 
@@ -18,6 +19,15 @@ class LHS::Service
18
19
  endpoint = LHC::Endpoint.new(url, options)
19
20
  instance.sanity_check(endpoint)
20
21
  instance.endpoints.push(endpoint)
22
+ LHS::Service::Endpoints.all ||= {}
23
+ LHS::Service::Endpoints.all[url] = instance
24
+ end
25
+
26
+ def for_url(url)
27
+ template, service = LHS::Service::Endpoints.all.detect do |template, _service|
28
+ LHC::Endpoint.match?(url, template)
29
+ end
30
+ service
21
31
  end
22
32
  end
23
33
 
@@ -15,38 +15,34 @@ class LHS::Service
15
15
 
16
16
  private
17
17
 
18
- def multiple_requests(options)
19
- options.map { |options| process_options(options) }
20
- responses = LHC.request(options)
21
- data = responses.map{ |response| LHS::Data.new(response.body, nil, self.class, response.request) }
22
- data = LHS::Data.new(data, nil, self.class)
23
- handle_includes(data) if includes
24
- data
18
+ # Convert URLs in options to endpoint templates
19
+ def convert_options_to_endpoints(options)
20
+ if options.respond_to?(:map)
21
+ options.map { |option| convert_option_to_endpoints(option) }
22
+ else
23
+ convert_option_to_endpoints(options)
24
+ end
25
25
  end
26
26
 
27
- def single_request(options)
28
- response = LHC.request(process_options(options))
29
- data = LHS::Data.new(response.body, nil, self.class, response.request)
30
- handle_includes(data) if includes
31
- data
27
+ def convert_option_to_endpoints(option)
28
+ new_options = option.dup
29
+ url = option[:url]
30
+ return unless endpoint = LHS::Endpoint.for_url(url)
31
+ template = endpoint.url
32
+ new_options = new_options.merge(params: LHC::Endpoint.values_as_params(template, url))
33
+ new_options[:url] = template
34
+ new_options
32
35
  end
33
36
 
34
- # Merge explicit params and take configured endpoints options as base
35
- def process_options(options)
36
- endpoint = find_endpoint(options[:params])
37
- options = (endpoint.options || {}).merge(options)
38
- options[:url] = compute_url!(options[:params]) unless options.key?(:url)
39
- merge_explicit_params!(options[:params])
40
- options.delete(:params) if options[:params] && options[:params].empty?
41
- options
42
- end
43
-
44
- # Merge explicit params nested in 'params' namespace with original hash.
45
- def merge_explicit_params!(params)
46
- return true unless params
47
- explicit_params = params[:params]
48
- params.delete(:params)
49
- params.merge!(explicit_params) if explicit_params
37
+ def extend(data, addition, key)
38
+ if data._proxy.is_a? LHS::Collection
39
+ data.each_with_index do |item, i|
40
+ item = item[i] if item.is_a? LHS::Collection
41
+ item._raw[key.to_s].merge!(addition[i]._raw)
42
+ end
43
+ elsif data._proxy.is_a? LHS::Item
44
+ data._raw[key.to_s].merge!(addition._raw)
45
+ end
50
46
  end
51
47
 
52
48
  def handle_includes(data)
@@ -59,36 +55,77 @@ class LHS::Service
59
55
 
60
56
  def handle_include(data, key)
61
57
  options = if data._proxy.is_a? LHS::Collection
62
- include_multiple(data, key)
58
+ options_for_multiple(data, key)
63
59
  else
64
- include_single(data, key)
60
+ url_option_for(data, key)
65
61
  end
62
+ service = service_for_options(options) || self
63
+ options = convert_options_to_endpoints(options) if service_for_options(options)
66
64
  addition = if (further_keys = includes.fetch(key, nil) if includes.is_a? Hash)
67
- self.class.includes(further_keys).instance.request(options)
65
+ service.class.includes(further_keys).instance.request(options)
68
66
  else
69
- self.class.includes(nil).instance.request(options)
67
+ service.class.includes(nil).instance.request(options)
70
68
  end
71
69
  extend(data, addition, key)
72
70
  end
73
71
 
74
- def extend(data, addition, key)
75
- if data._proxy.is_a? LHS::Collection
76
- data.each_with_index do |item, i|
77
- item = item[i] if item.is_a? LHS::Collection
78
- item._raw[key.to_s].merge!(addition[i]._raw)
79
- end
80
- elsif data._proxy.is_a? LHS::Item
81
- data._raw[key.to_s].merge!(addition._raw)
82
- end
72
+ # Merge explicit params nested in 'params' namespace with original hash.
73
+ def merge_explicit_params!(params)
74
+ return true unless params
75
+ explicit_params = params[:params]
76
+ params.delete(:params)
77
+ params.merge!(explicit_params) if explicit_params
78
+ end
79
+
80
+ def multiple_requests(options)
81
+ options = options.map { |options| process_options(options) }
82
+ responses = LHC.request(options)
83
+ data = responses.map{ |response| LHS::Data.new(response.body, nil, self.class, response.request) }
84
+ data = LHS::Data.new(data, nil, self.class)
85
+ handle_includes(data) if includes
86
+ data
83
87
  end
84
88
 
85
- def include_multiple(data, key)
89
+ def options_for_multiple(data, key)
86
90
  data.map do |item|
87
- include_single(item, key)
91
+ url_option_for(item, key)
88
92
  end
89
93
  end
90
94
 
91
- def include_single(item, key)
95
+ # Merge explicit params and take configured endpoints options as base
96
+ def process_options(options)
97
+ options ||= {}
98
+ options = options.dup
99
+ endpoint = find_endpoint(options[:params])
100
+ options = (endpoint.options || {}).merge(options)
101
+ options[:url] = compute_url!(options[:params]) unless options.key?(:url)
102
+ merge_explicit_params!(options[:params])
103
+ options.delete(:params) if options[:params] && options[:params].empty?
104
+ options
105
+ end
106
+
107
+ def service_for_options(options)
108
+ services = []
109
+ if options.is_a?(Array)
110
+ options.each do |option|
111
+ next unless service = LHS::Service.for_url(option[:url])
112
+ services.push(service)
113
+ end
114
+ fail 'Found more than one service that could be used to do the request' if services.uniq.count > 1
115
+ services.uniq.first
116
+ else # Hash
117
+ LHS::Service.for_url(options[:url])
118
+ end
119
+ end
120
+
121
+ def single_request(options)
122
+ response = LHC.request(process_options(options))
123
+ data = LHS::Data.new(response.body, nil, self.class, response.request)
124
+ handle_includes(data) if includes
125
+ data
126
+ end
127
+
128
+ def url_option_for(item, key)
92
129
  link = item[key]
93
130
  { url: link.href }
94
131
  end
@@ -0,0 +1,11 @@
1
+ # An endpoint is used as source to fetch objects
2
+ class LHS::Endpoint
3
+
4
+ def self.for_url(url)
5
+ template, service = LHS::Service::Endpoints.all.detect do |template, _service|
6
+ LHC::Endpoint.match?(url, template)
7
+ end
8
+ service.endpoints.detect { |endpoint| endpoint.url == template } if service
9
+ end
10
+ end
11
+
@@ -1,3 +1,3 @@
1
1
  module LHS
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,27 @@
1
+ require 'rails_helper'
2
+
3
+ describe LHS::Endpoint do
4
+
5
+ context 'for url' do
6
+
7
+ before(:each) do
8
+ class SomeService < LHS::Service
9
+ endpoint ':datastore/entries/:entry_id/content-ads/:campaign_id/feedbacks'
10
+ endpoint ':datastore/:campaign_id/feedbacks'
11
+ endpoint ':datastore/feedbacks'
12
+ end
13
+ end
14
+
15
+ it 'provides the endpoint for a given url' do
16
+ expect(
17
+ LHS::Endpoint.for_url('http://datastore.local.ch/v2/entries/123/content-ads/456/feedbacks').url
18
+ ).to eq ':datastore/entries/:entry_id/content-ads/:campaign_id/feedbacks'
19
+ expect(
20
+ LHS::Endpoint.for_url('http://datastore.local.ch/123/feedbacks').url
21
+ ).to eq ':datastore/:campaign_id/feedbacks'
22
+ expect(
23
+ LHS::Endpoint.for_url('http://datastore.local.ch/feedbacks').url
24
+ ).to eq ':datastore/feedbacks'
25
+ end
26
+ end
27
+ end
@@ -15,6 +15,12 @@ describe LHS::Service do
15
15
  end
16
16
  end
17
17
 
18
+ it 'stores all the endpoints by url' do
19
+ expect(LHS::Service::Endpoints.all[':datastore/entries/:entry_id/content-ads/:campaign_id/feedbacks']).to be
20
+ expect(LHS::Service::Endpoints.all[':datastore/:campaign_id/feedbacks']).to be
21
+ expect(LHS::Service::Endpoints.all[':datastore/feedbacks']).to be
22
+ end
23
+
18
24
  it 'stores the endpoints of the service' do
19
25
  expect(SomeService.instance.endpoints.count).to eq 3
20
26
  expect(SomeService.instance.endpoints[0].url).to eq ':datastore/entries/:entry_id/content-ads/:campaign_id/feedbacks'
@@ -5,40 +5,40 @@ describe LHS::Service do
5
5
  let(:datastore) { 'http://datastore-stg.lb-service.sunrise.intra.local.ch/v2' }
6
6
  before(:each) { LHC.config.placeholder('datastore', datastore) }
7
7
 
8
+ let(:stub_campaign_request) do
9
+ stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
10
+ .to_return(body: {
11
+ 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d",
12
+ 'entry' => { 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va" }
13
+ }.to_json)
14
+ end
15
+
16
+ let(:stub_entry_request) do
17
+ stub_request(:get, "#{datastore}/local-entries/lakj35asdflkj1203va")
18
+ .to_return(body: { 'name' => 'Casa Ferlin' }.to_json)
19
+ end
20
+
8
21
  context 'includes' do
9
22
 
10
23
  before(:each) do
11
24
  class Feedback < LHS::Service
12
25
  endpoint ':datastore/feedbacks'
13
26
  end
14
-
15
- stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
16
- .to_return(status: 200, body: {
17
- "href" => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d",
18
- "entry" => {
19
- "href" => "#{datastore}/local-entries/lakj35asdflkj1203va"
20
- }
21
- }.to_json)
22
-
23
- stub_request(:get, "#{datastore}/local-entries/lakj35asdflkj1203va")
24
- .to_return(status: 200, body: {
25
- "name" => 'Casa Ferlin'
26
- }.to_json)
27
+ stub_campaign_request
28
+ stub_entry_request
27
29
  end
28
30
 
29
31
  it 'includes linked resources while fetching multiple resources from one service' do
30
32
 
31
33
  stub_request(:get, "#{datastore}/feedbacks?has_reviews=true")
32
- .to_return(status: 200, body: {
33
- items:[
34
+ .to_return(status: 200, body: {
35
+ items: [
34
36
  {
35
- "href" => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
36
- "campaign" => {
37
- "href" => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d"
38
- }
37
+ 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
38
+ 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
39
39
  }
40
40
  ]
41
- }.to_json)
41
+ }.to_json)
42
42
 
43
43
  feedbacks = Feedback.includes(campaign: :entry).where(has_reviews: true)
44
44
  expect(feedbacks.first.campaign.entry.name).to eq 'Casa Ferlin'
@@ -47,15 +47,49 @@ describe LHS::Service do
47
47
  it 'includes linked resources while fetching a single resource from one service' do
48
48
 
49
49
  stub_request(:get, "#{datastore}/feedbacks/123")
50
- .to_return(status: 200, body: {
51
- "href" => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
52
- "campaign" => {
53
- "href" => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d"
54
- }
55
- }.to_json)
50
+ .to_return(status: 200, body: {
51
+ 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
52
+ 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
53
+ }.to_json)
56
54
 
57
55
  feedbacks = Feedback.includes(campaign: :entry).find(123)
58
56
  expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
59
57
  end
58
+
59
+ context 'include objects from known services' do
60
+
61
+ let(:stub_feedback_request) do
62
+ stub_request(:get, "#{datastore}/feedbacks")
63
+ .to_return(status: 200, body: {
64
+ items: [
65
+ {
66
+ 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
67
+ 'entry' => {
68
+ 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va"
69
+ }
70
+ }
71
+ ]
72
+ }.to_json)
73
+ end
74
+
75
+ before(:each) do
76
+ class Entry < LHS::Service
77
+ endpoint ':datastore/local-entries/:id'
78
+ end
79
+ class SomeInterceptor < LHC::Interceptor; end
80
+ LHC.config.interceptors = [SomeInterceptor]
81
+ end
82
+
83
+ it 'uses interceptors for included links from known services' do
84
+ stub_feedback_request
85
+ stub_entry_request
86
+
87
+ @called = 0
88
+ allow_any_instance_of(SomeInterceptor).to receive(:before_request) { @called += 1 }
89
+
90
+ expect(Feedback.includes(:entry).where.first.entry.name).to eq 'Casa Ferlin'
91
+ expect(@called).to eq 2
92
+ end
93
+ end
60
94
  end
61
95
  end
@@ -0,0 +1,6 @@
1
+ RSpec.configure do |config|
2
+
3
+ config.before(:each) do
4
+ LHS::Service::Endpoints.all = {}
5
+ end
6
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - local.ch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-28 00:00:00.000000000 Z
11
+ date: 2015-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lhc
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.0
19
+ version: 1.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.0
26
+ version: 1.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: lhc-core-interceptors
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -148,6 +148,7 @@ files:
148
148
  - lib/lhs/concerns/service/request.rb
149
149
  - lib/lhs/concerns/service/where.rb
150
150
  - lib/lhs/data.rb
151
+ - lib/lhs/endpoint.rb
151
152
  - lib/lhs/errors.rb
152
153
  - lib/lhs/item.rb
153
154
  - lib/lhs/proxy.rb
@@ -204,6 +205,7 @@ files:
204
205
  - spec/dummy/public/422.html
205
206
  - spec/dummy/public/500.html
206
207
  - spec/dummy/public/favicon.ico
208
+ - spec/endpoint/for_url_spec.rb
207
209
  - spec/item/destroy_spec.rb
208
210
  - spec/item/getter_spec.rb
209
211
  - spec/item/respond_to_spec.rb
@@ -232,6 +234,7 @@ files:
232
234
  - spec/service/where_spec.rb
233
235
  - spec/spec_helper.rb
234
236
  - spec/support/cleanup_configuration.rb
237
+ - spec/support/cleanup_endpoints.rb
235
238
  - spec/support/cleanup_services.rb
236
239
  - spec/support/fixtures/json/feedback.json
237
240
  - spec/support/fixtures/json/feedbacks.json
@@ -311,6 +314,7 @@ test_files:
311
314
  - spec/dummy/public/422.html
312
315
  - spec/dummy/public/500.html
313
316
  - spec/dummy/public/favicon.ico
317
+ - spec/endpoint/for_url_spec.rb
314
318
  - spec/item/destroy_spec.rb
315
319
  - spec/item/getter_spec.rb
316
320
  - spec/item/respond_to_spec.rb
@@ -339,6 +343,7 @@ test_files:
339
343
  - spec/service/where_spec.rb
340
344
  - spec/spec_helper.rb
341
345
  - spec/support/cleanup_configuration.rb
346
+ - spec/support/cleanup_endpoints.rb
342
347
  - spec/support/cleanup_services.rb
343
348
  - spec/support/fixtures/json/feedback.json
344
349
  - spec/support/fixtures/json/feedbacks.json