rom-http 0.5.0 → 0.6.0.rc1

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.
@@ -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