rom-http 0.5.0 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  module ROM
2
2
  module HTTP
3
3
  Error = Class.new(StandardError)
4
+ SchemaNotDefinedError = Class.new(Error)
4
5
  end
5
6
  end
@@ -1,4 +1,4 @@
1
- require 'thread_safe'
1
+ require 'concurrent'
2
2
  require 'rom/http/dataset'
3
3
 
4
4
  module ROM
@@ -29,7 +29,7 @@ module ROM
29
29
  #
30
30
  # @api public
31
31
  def initialize(config)
32
- @datasets = ThreadSafe::Cache.new
32
+ @datasets = Concurrent::Map.new
33
33
  @config = config
34
34
  end
35
35
 
@@ -1,46 +1,123 @@
1
- require 'rom/plugins/relation/view'
1
+ require 'dry/core/cache'
2
+ require 'rom/initializer'
2
3
  require 'rom/plugins/relation/key_inference'
4
+ require 'rom/http/transformer'
3
5
 
4
6
  module ROM
5
7
  module HTTP
6
8
  # HTTP-specific relation extensions
7
9
  #
8
10
  class Relation < ROM::Relation
11
+ extend Dry::Core::Cache
12
+ extend ::ROM::Initializer
9
13
  include Enumerable
10
14
 
11
15
  adapter :http
12
16
 
13
- use :view
14
17
  use :key_inference
15
18
 
19
+ option :transformer, reader: true, default: proc { ::ROM::HTTP::Transformer }
20
+
16
21
  forward :with_request_method, :with_path, :append_path, :with_options,
17
- :with_params, :clear_params, :project
22
+ :with_params, :clear_params
23
+
18
24
 
19
- # @api private
20
25
  def initialize(*)
21
26
  super
22
- if schema?
23
- dataset.response_transformer(
24
- Dataset::ResponseTransformers::Schemad.new(schema.to_h)
25
- )
27
+
28
+ raise(
29
+ SchemaNotDefinedError,
30
+ "You must define a schema for #{self.class.register_as} relation"
31
+ ) unless schema?
32
+ end
33
+
34
+ def primary_key
35
+ attribute = schema.find(&:primary_key?)
36
+
37
+ if attribute
38
+ attribute.alias || attribute.name
39
+ else
40
+ :id
26
41
  end
27
42
  end
28
43
 
44
+ def project(*names)
45
+ with(schema: schema.project(*names.flatten))
46
+ end
47
+
48
+ def exclude(*names)
49
+ with(schema: schema.exclude(*names.flatten))
50
+ end
51
+
52
+ def rename(mapping)
53
+ with(schema: schema.rename(mapping))
54
+ end
55
+
56
+ def prefix(prefix)
57
+ with(schema: schema.prefix(prefix))
58
+ end
59
+
60
+ def wrap(prefix = dataset.name)
61
+ with(schema: schema.wrap(prefix))
62
+ end
63
+
64
+ def to_a
65
+ with_transformation { super }
66
+ end
67
+
29
68
  # @see Dataset#insert
30
69
  def insert(*args)
31
- dataset.insert(*args)
70
+ with_transformation { dataset.insert(*args) }
32
71
  end
33
72
  alias_method :<<, :insert
34
73
 
35
74
  # @see Dataset#update
36
75
  def update(*args)
37
- dataset.update(*args)
76
+ with_transformation { dataset.update(*args) }
38
77
  end
39
78
 
40
79
  # @see Dataset#delete
41
80
  def delete
42
81
  dataset.delete
43
82
  end
83
+
84
+ private
85
+
86
+ def with_transformation(&block)
87
+ tuples = block.call
88
+
89
+ transformed = with_schema_proc do |proc|
90
+ transformer_proc[Array([tuples]).flatten(1).map(&proc.method(:call))]
91
+ end
92
+
93
+ tuples.kind_of?(Array) ? transformed : transformed.first
94
+ end
95
+
96
+ def with_schema_proc(&block)
97
+ schema_proc = fetch_or_store(schema) do
98
+ Types::Coercible::Hash.schema(schema.to_h)
99
+ end
100
+
101
+ block.call(schema_proc)
102
+ end
103
+
104
+ def transformer_proc
105
+ if mapped?
106
+ transformer[:map_array, transformer[:rename_keys, mapping]]
107
+ else
108
+ transformer[:identity]
109
+ end
110
+ end
111
+
112
+ def mapped?
113
+ mapping.any?
114
+ end
115
+
116
+ def mapping
117
+ schema.each_with_object({}) do |attr, mapping|
118
+ mapping[attr.name] = attr.alias if attr.alias
119
+ end
120
+ end
44
121
  end
45
122
  end
46
123
  end
@@ -0,0 +1,16 @@
1
+ module ROM
2
+ module HTTP
3
+ # Transformer
4
+ #
5
+ # Used to perform data transformations on behalf of relations
6
+ #
7
+ # @api private
8
+ module Transformer
9
+ extend Transproc::Registry
10
+
11
+ import :identity, from: ::Transproc::Coercions
12
+ import :map_array, from: ::Transproc::ArrayTransformations
13
+ import :rename_keys, from: ::Transproc::HashTransformations
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module HTTP
3
- VERSION = '0.5.0'.freeze
3
+ VERSION = '0.6.0.rc1'.freeze
4
4
  end
5
5
  end
@@ -18,12 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_runtime_dependency 'thread_safe'
22
- spec.add_runtime_dependency 'dry-types'
23
- spec.add_runtime_dependency 'rom', '~> 2.0'
24
- spec.add_runtime_dependency 'dry-equalizer'
21
+ spec.add_runtime_dependency 'concurrent-ruby'
22
+ spec.add_runtime_dependency 'rom', '~> 3.0.0.rc'
23
+ spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
24
+ spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
25
25
 
26
26
  spec.add_development_dependency 'bundler'
27
27
  spec.add_development_dependency 'rspec', '~> 3.1'
28
+ spec.add_development_dependency 'rspec-its'
28
29
  spec.add_development_dependency 'rake', '~> 10.0'
29
30
  end
@@ -2,14 +2,22 @@ RSpec.describe ROM::HTTP::Commands::Create do
2
2
  include_context 'setup'
3
3
  let(:relation) do
4
4
  Class.new(ROM::HTTP::Relation) do
5
- dataset :users
5
+ schema(:users) do
6
+ attribute :id, ROM::Types::Int
7
+ attribute :first_name, ROM::Types::String
8
+ attribute :last_name, ROM::Types::String
9
+ end
10
+
11
+ def by_id(id)
12
+ with_params(id: id)
13
+ end
6
14
  end
7
15
  end
8
16
 
9
17
  context 'with single tuple' do
10
18
  let(:response) { double }
11
- let(:tuple) { double }
12
19
  let(:attributes) { { first_name: 'John', last_name: 'Jackson' } }
20
+ let(:tuple) { attributes.merge(id: 1) }
13
21
  let(:command) do
14
22
  Class.new(ROM::HTTP::Commands::Create) do
15
23
  register_as :create
@@ -51,10 +59,10 @@ RSpec.describe ROM::HTTP::Commands::Create do
51
59
  context 'with a collection' do
52
60
  let(:response_1) { double }
53
61
  let(:response_2) { double }
54
- let(:tuple_1) { double }
55
- let(:tuple_2) { double }
56
62
  let(:attributes_1) { { first_name: 'John', last_name: 'Jackson' } }
57
63
  let(:attributes_2) { { first_name: 'Jill', last_name: 'Smith' } }
64
+ let(:tuple_1) { attributes_1.merge(id: 1) }
65
+ let(:tuple_2) { attributes_2.merge(id: 2) }
58
66
  let(:attributes) { [attributes_1, attributes_2] }
59
67
  let(:command) do
60
68
  Class.new(ROM::HTTP::Commands::Create) do
@@ -2,7 +2,13 @@ RSpec.describe ROM::HTTP::Commands::Delete do
2
2
  include_context 'setup'
3
3
  let(:relation) do
4
4
  Class.new(ROM::HTTP::Relation) do
5
- dataset :users
5
+ schema(:users) do
6
+ attribute :id, ROM::Types::Int
7
+ end
8
+
9
+ def by_id(id)
10
+ with_params(id: id)
11
+ end
6
12
  end
7
13
  end
8
14
  let(:response) { double }
@@ -2,14 +2,22 @@ RSpec.describe ROM::HTTP::Commands::Update do
2
2
  include_context 'setup'
3
3
  let(:relation) do
4
4
  Class.new(ROM::HTTP::Relation) do
5
- dataset :users
5
+ schema(:users) do
6
+ attribute :id, ROM::Types::Int
7
+ attribute :first_name, ROM::Types::String
8
+ attribute :last_name, ROM::Types::String
9
+ end
10
+
11
+ def by_id(id)
12
+ with_params(id: id)
13
+ end
6
14
  end
7
15
  end
8
16
 
9
17
  context 'with single tuple' do
10
18
  let(:response) { double }
11
- let(:tuple) { double }
12
19
  let(:attributes) { { first_name: 'John', last_name: 'Jackson' } }
20
+ let(:tuple) { attributes.merge(id: 1) }
13
21
  let(:command) do
14
22
  Class.new(ROM::HTTP::Commands::Update) do
15
23
  register_as :update
@@ -51,10 +59,10 @@ RSpec.describe ROM::HTTP::Commands::Update do
51
59
  context 'with a collection' do
52
60
  let(:response_1) { double }
53
61
  let(:response_2) { double }
54
- let(:tuple_1) { double }
55
- let(:tuple_2) { double }
56
62
  let(:attributes_1) { { first_name: 'John', last_name: 'Jackson' } }
57
63
  let(:attributes_2) { { first_name: 'Jill', last_name: 'Smith' } }
64
+ let(:tuple_1) { attributes_1.merge(id: 1) }
65
+ let(:tuple_2) { attributes_2.merge(id: 2) }
58
66
  let(:attributes) { [attributes_1, attributes_2] }
59
67
  let(:command) do
60
68
  Class.new(ROM::HTTP::Commands::Update) do
@@ -8,10 +8,9 @@ RSpec.describe ROM::HTTP::Relation do
8
8
 
9
9
  let(:relation) do
10
10
  Class.new(ROM::HTTP::Relation) do
11
- dataset :users
12
-
13
- view :base, [:id, :name] do
14
- self
11
+ schema(:users) do
12
+ attribute :id, ROM::Types::Int
13
+ attribute :name, ROM::Types::String
15
14
  end
16
15
 
17
16
  def by_id(id)
@@ -38,8 +37,7 @@ RSpec.describe ROM::HTTP::Relation do
38
37
  response_handler: response_handler,
39
38
  name: :users
40
39
  },
41
- request_method: :get,
42
- path: "/#{id}",
40
+ path: "users/#{id}",
43
41
  params: params
44
42
  )
45
43
  end
@@ -2,7 +2,9 @@ RSpec.shared_context 'users and tasks' do
2
2
  include_context 'setup'
3
3
  let(:users_relation) do
4
4
  Class.new(ROM::HTTP::Relation) do
5
- dataset :users
5
+ schema(:users) do
6
+ attribute :id, ROM::Types::Int
7
+ end
6
8
 
7
9
  def by_id(id)
8
10
  with_params(id: id)
@@ -11,7 +13,9 @@ RSpec.shared_context 'users and tasks' do
11
13
  end
12
14
  let(:tasks_relation) do
13
15
  Class.new(ROM::HTTP::Relation) do
14
- dataset :tasks
16
+ schema(:tasks) do
17
+ attribute :id, ROM::Types::Int
18
+ end
15
19
 
16
20
  def by_id(id)
17
21
  with_params(id: id)
@@ -4,6 +4,7 @@ require 'bundler'
4
4
  Bundler.setup
5
5
 
6
6
  require 'rom-http'
7
+ require 'rspec/its'
7
8
 
8
9
  begin
9
10
  require 'byebug'
@@ -22,8 +22,8 @@ RSpec.describe ROM::HTTP::Dataset do
22
22
  it { is_expected.to eq(config) }
23
23
  end
24
24
 
25
- describe '#options' do
26
- subject { dataset.options }
25
+ describe 'options' do
26
+ subject { dataset }
27
27
 
28
28
  context 'with options passed' do
29
29
  let(:options) do
@@ -35,29 +35,19 @@ RSpec.describe ROM::HTTP::Dataset do
35
35
  }
36
36
  end
37
37
 
38
- it do
39
- is_expected.to eq(
40
- request_method: :put,
41
- path: '',
42
- projections: [],
43
- params: {},
44
- headers: {
45
- 'Accept' => 'application/json'
46
- }
47
- )
48
- end
38
+ its(:base_path) { is_expected.to eq('') }
39
+ its(:request_method) { is_expected.to eq(:put) }
40
+ its(:path) { is_expected.to eq('') }
41
+ its(:params) { is_expected.to eq({}) }
42
+ its(:headers) { is_expected.to eq('Accept' => 'application/json') }
49
43
  end
50
44
 
51
45
  context 'with no options passed' do
52
- it do
53
- is_expected.to eq(
54
- request_method: :get,
55
- path: '',
56
- projections: [],
57
- params: {},
58
- headers: {}
59
- )
60
- end
46
+ its(:base_path) { is_expected.to eq('') }
47
+ its(:request_method) { is_expected.to eq(:get) }
48
+ its(:path) { is_expected.to eq('') }
49
+ its(:params) { is_expected.to eq({}) }
50
+ its(:headers) { is_expected.to eq({}) }
61
51
  end
62
52
  end
63
53
  end
@@ -110,40 +100,6 @@ RSpec.describe ROM::HTTP::Dataset do
110
100
  end
111
101
  end
112
102
 
113
- describe '#response_transformer' do
114
- context 'with argument' do
115
- let(:transformer) { double('ResponseTransformer') }
116
-
117
- subject! { dataset.response_transformer(transformer) }
118
-
119
- it do
120
- expect(dataset.instance_variable_get(:@response_transformer))
121
- .to eq(transformer)
122
- end
123
- it { is_expected.to eq(transformer) }
124
- end
125
-
126
- context 'without argument' do
127
- context 'when transformer not set' do
128
- subject! { dataset.response_transformer }
129
-
130
- it { is_expected.to be_a(ROM::HTTP::Dataset::ResponseTransformers::Schemaless) }
131
- end
132
-
133
- context 'when transformer set' do
134
- let(:transformer) { double('ResponseTransformer') }
135
-
136
- before do
137
- dataset.response_transformer(transformer)
138
- end
139
-
140
- subject! { dataset.response_transformer }
141
-
142
- it { is_expected.to eq(transformer) }
143
- end
144
- end
145
- end
146
-
147
103
  describe '#uri' do
148
104
  context 'when no uri configured' do
149
105
  let(:config) { {} }
@@ -219,6 +175,51 @@ RSpec.describe ROM::HTTP::Dataset do
219
175
  end
220
176
  end
221
177
 
178
+ describe '#base_path' do
179
+ subject { dataset.base_path }
180
+
181
+ context 'with no base_path option' do
182
+ context 'when dataset name is set' do
183
+ let(:config) do
184
+ {
185
+ uri: uri,
186
+ request_handler: request_handler,
187
+ response_handler: response_handler,
188
+ name: :users
189
+ }
190
+ end
191
+
192
+ it 'returns the dataset name as a string' do
193
+ is_expected.to eq('users')
194
+ end
195
+ end
196
+
197
+ context 'when dataset name is not set' do
198
+ it 'returns an empty string' do
199
+ is_expected.to eq('')
200
+ end
201
+ end
202
+ end
203
+
204
+ context 'with base_path option' do
205
+ context 'when base_path is absolute' do
206
+ let(:base_path) { '/users' }
207
+ let(:options) { { base_path: base_path } }
208
+
209
+ it 'removes the leading /' do
210
+ is_expected.to eq('users')
211
+ end
212
+ end
213
+
214
+ context 'when base_path is not absolute' do
215
+ let(:base_path) { 'users' }
216
+ let(:options) { { base_path: base_path } }
217
+
218
+ it { is_expected.to eq(base_path) }
219
+ end
220
+ end
221
+ end
222
+
222
223
  describe '#path' do
223
224
  subject { dataset.path }
224
225
 
@@ -305,16 +306,13 @@ RSpec.describe ROM::HTTP::Dataset do
305
306
 
306
307
  subject! { new_dataset }
307
308
 
308
- it { expect(new_dataset.config).to eq(config) }
309
- it do
310
- expect(new_dataset.options).to eq(
311
- request_method: :get,
312
- path: '',
313
- projections: [],
314
- params: {},
315
- headers: headers
316
- )
317
- end
309
+ its(:config) { is_expected.to eq(config) }
310
+ its(:base_path) { is_expected.to eq('') }
311
+ its(:request_method) { is_expected.to eq(:get) }
312
+ its(:path) { is_expected.to eq('') }
313
+ its(:params) { is_expected.to eq({}) }
314
+ its(:headers) { is_expected.to eq(headers) }
315
+
318
316
  it { is_expected.to_not be(dataset) }
319
317
  it { is_expected.to be_a(ROM::HTTP::Dataset) }
320
318
  end
@@ -360,68 +358,29 @@ RSpec.describe ROM::HTTP::Dataset do
360
358
 
361
359
  subject! { new_dataset }
362
360
 
363
- it { expect(new_dataset.config).to eq(config) }
364
- it do
365
- expect(new_dataset.options).to eq(
366
- request_method: :get,
367
- path: '',
368
- projections: [],
369
- params: {
370
- name: name
371
- },
372
- headers: {}
373
- )
374
- end
361
+ its(:config) { is_expected.to eq(config) }
362
+ its(:base_path) { is_expected.to eq('') }
363
+ its(:request_method) { is_expected.to eq(:get) }
364
+ its(:path) { is_expected.to eq('') }
365
+ its(:params) { is_expected.to eq(name: name) }
366
+ its(:headers) { is_expected.to eq({}) }
367
+
375
368
  it { is_expected.to_not be(dataset) }
376
369
  it { is_expected.to be_a(ROM::HTTP::Dataset) }
377
370
  end
378
371
 
379
- describe '#project' do
380
- let(:data) do
381
- [
382
- { id: 1, name: 'John', email: 'john@hotmail.com' },
383
- { id: 2, name: 'Jill', email: 'jill@hotmail.com' }
384
- ]
385
- end
372
+ describe '#with_base_path' do
373
+ let(:base_path) { '/users/tasks' }
374
+ let(:new_dataset) { double(ROM::HTTP::Dataset) }
386
375
 
387
376
  before do
388
- allow(request_handler).to receive(:call)
389
- allow(response_handler).to receive(:call).and_return(data)
390
- end
391
-
392
- subject! { dataset.project(*projections).to_a }
393
-
394
- context 'with projections' do
395
- context 'with a list of arguments' do
396
- let(:projections) { [:id, :name] }
397
-
398
- it 'applies the projections to the result set' do
399
- is_expected.to match_array([
400
- { id: 1, name: 'John' },
401
- { id: 2, name: 'Jill' }
402
- ])
403
- end
404
- end
405
-
406
- context 'with a single array argument' do
407
- let(:projections) { [[:id, :name]] }
408
-
409
- it 'applies the projections to the result set' do
410
- is_expected.to match_array([
411
- { id: 1, name: 'John' },
412
- { id: 2, name: 'Jill' }
413
- ])
414
- end
415
- end
377
+ allow(dataset).to receive(:with_options).and_return(new_dataset)
416
378
  end
417
379
 
418
- context 'without projections' do
419
- let(:projections) { [] }
380
+ subject! { dataset.with_base_path(base_path) }
420
381
 
421
- it 'returns the original data' do
422
- is_expected.to match_array(data)
423
- end
424
- end
382
+ it { expect(dataset).to have_received(:with_options).with(base_path: base_path) }
383
+ it { is_expected.to eq(new_dataset) }
425
384
  end
426
385
 
427
386
  describe '#with_path' do
@@ -449,14 +408,14 @@ RSpec.describe ROM::HTTP::Dataset do
449
408
  subject! { dataset.append_path(path) }
450
409
 
451
410
  context 'without existing path' do
452
- it { expect(dataset).to have_received(:with_options).with(path: '/tasks') }
411
+ it { expect(dataset).to have_received(:with_options).with(path: 'tasks') }
453
412
  it { is_expected.to eq(new_dataset) }
454
413
  end
455
414
 
456
415
  context 'with existing path' do
457
416
  let(:options) { { path: '/users' } }
458
417
 
459
- it { expect(dataset).to have_received(:with_options).with(path: '/users/tasks') }
418
+ it { expect(dataset).to have_received(:with_options).with(path: 'users/tasks') }
460
419
  it { is_expected.to eq(new_dataset) }
461
420
  end
462
421
  end