luna_park 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41e8a50e90bb5016f430dc2073ffa3875f70d87f9202487623196feb7954c630
4
- data.tar.gz: fb1bb8ce6e7d840bccc7b0ea15b9c88a54c93a2acbdf4d76c482de05a5063c8c
3
+ metadata.gz: 3491b1852cab029d5f1b60cdc9ff638e2eac71a1d366e1463e31b7c6a4afa127
4
+ data.tar.gz: f0b5abc0af74a11ac5d6e392def3daa5e565ac7ba5033315981013dae3f0734a
5
5
  SHA512:
6
- metadata.gz: '075301879b59ac53b9881a398715ac4ea1b071b5c581fdb1046ca57f7ddea9450360ddd57fac674c2c1fc9b28838ad55cfd0dcab494e380b4b94188e2acba879'
7
- data.tar.gz: 7bf3b42b59dbed4b81e77f23a9f51a4bcca4699f98310a9bdb2d5a8b5385f50862f2f215be780832104fa983df3c1851c8165f4353fd9718d26b06b34dc87325
6
+ metadata.gz: 25f821e41e63fcb3bfcc8744ea203b861360126cc7edc12f5032e7153831a514e527ef80e8453fc5b8f24e5bf5f458c88226f1103ab8bcf85249222d4c7355b5
7
+ data.tar.gz: 903dd8923a134ea14d8f3611e3c803989f976f923e5b251cc65e47bef6cecc923173ae6654f2361ac2cc49190231d7d24c427d4144720abdbd9d0368b5ae386b
data/.rubocop.yml CHANGED
@@ -50,7 +50,7 @@ Style/Documentation:
50
50
  Enabled: false
51
51
 
52
52
  Naming/MethodParameterName:
53
- AllowedNames: io, id, to, by, on, in, at, ip, db, pk, fk
53
+ AllowedNames: io, id, to, by, on, in, at, ip, db, pk, fk, ds
54
54
 
55
55
  # TODO: поговорить с Филиппом про attr
56
56
  Layout/EmptyLinesAroundAttributeAccessor:
data/CHANGELOG.md CHANGED
@@ -4,13 +4,36 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.12.0] - 2023-02-23
7
+ ## [0.13.0] - 2023-06-06
8
8
  Changed
9
+ - `Extensions::Repositories::Postgres::Delete` returns boolean
10
+ - DataMapper became more safe: it will raise `MoreThanOneRecord` if `#read_one` received array with multiple items,
11
+ so it will show you your critical logic mistake
12
+
13
+ Added
14
+ - `DataMapper.mapper` now can receive block instead of class, to describe anonymous mapper;
15
+ default parent can be customized by defining `base_anonymous_mapper` method
16
+ - `DataMapper.entity` now have second argument to customize `coercion` - you can use your own Entity class (even Dry Struct)
17
+ - DataMapper became more safe: it will not try to transform hash of attributes to array of pairs
18
+ - Postgres extension `Create` now will set `updated_at` and `created_at` if exists
19
+ - Postgres extension `Update` now will set `updated_at` if exists
20
+ - Postgres extension `Read` now have default `#scope` abstract method to handle scoping options
21
+ - Postgres extension `Read` now have `#transaction` method
22
+ - Postgres extension `Read` now have `#lock(pk, &block)` method and `#lock!(pk, &block)`
23
+ - each Repository NotFound exception now have its own exception class, inherited from common NotFound exception
24
+ - `LunaPark::Mappers::Codirectional` now available as default mapper `LunaPark::Mapper`
25
+ - `LunaPark::Mappers::Codirectional` now can have nested mappers, and can be configured for arrays
26
+
27
+ Fixed
28
+ - DataMapper configuration (entity, mapper) now can be inherited. NotFound error also will be inherited.
29
+
30
+ ## [0.12.0] - 2023-02-23
31
+ Added
9
32
  - Added `TaggedLog`
10
33
 
11
34
  ## [0.11.7] - 2022-10-07
12
35
  Changed
13
- - Added `formatter` to `Notifiers::Log`. Using `format` in initializer is now deprecated.
36
+ - Added `formatter` to `Notifiers::Log`. Usage of `format` in initializer is now deprecated.
14
37
 
15
38
  ## [0.11.6] - 2021-10-06
16
39
  Changed
@@ -21,7 +44,7 @@ Added
21
44
  - add short alias for exceptions (`i18n:` instead of `i18n_key:`)
22
45
 
23
46
  ## [0.11.5] - 2022-09-27
24
- Changed
47
+ Added
25
48
  - Added `.custom_error` method to `Extensions::HasError` to define errors with a custom superclass
26
49
  - Added `#inject` method to `Extensions::Injector` - dependencies setter that allows you to create method chains
27
50
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- luna_park (0.12.1)
4
+ luna_park (0.13.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'luna_park/mappers/simple'
4
+
3
5
  module LunaPark
4
6
  module Extensions
5
7
  # @example
@@ -20,7 +22,7 @@ module LunaPark
20
22
  # def save(input)
21
23
  # entity = wrap(input)
22
24
  # row = to_row(entity)
23
- # new_row = products.where(id: entity.id).update(row)
25
+ # new_row = products.where(id: entity.id).returning.update(row)
24
26
  # new_attrs = from_row(new_row)
25
27
  # entity.set_attributes(new_attrs)
26
28
  # entity
@@ -36,33 +38,174 @@ module LunaPark
36
38
  # alias products dataset
37
39
  # end
38
40
  module DataMapper
39
- def self.included(base)
40
- base.extend ClassMethods
41
- base.include InstanceMethods
41
+ class << self
42
+ def extended(base)
43
+ base.include self
44
+ end
45
+
46
+ def included(base)
47
+ base.extend ClassMethods
48
+ base.include InstanceMethods
49
+
50
+ base.__define_constants__
51
+
52
+ defaults(base)
53
+ end
54
+
55
+ private
56
+
57
+ def defaults(base)
58
+ base.entity OpenStruct, :new
59
+ base.mapper LunaPark::Mappers::Simple
60
+ end
42
61
  end
43
62
 
44
63
  module ClassMethods
45
- attr_reader :entity_class, :mapper_class
64
+ attr_reader :entity_class, :mapper_class, :__entity_coercion__
46
65
 
47
66
  # Configure repository
48
67
 
49
- def entity(entity_class = nil)
50
- @entity_class = entity_class
68
+ # Configure tagret entity class and coercion for it
69
+ #
70
+ # @example default coercion for entity type than responds to .call
71
+ # class MyRepository
72
+ # entity MyEntity
73
+ # end
74
+ #
75
+ # input = { foo: 'FOO', bar: 'BAR' }
76
+ # MyRepository.new.send(:wrap, input) == MyEntity.call(input)
77
+ #
78
+ # @example default coercion for entity type than responds to .wrap
79
+ # class MyRepository
80
+ # entity MyEntity
81
+ # end
82
+ #
83
+ # input = { foo: 'FOO', bar: 'BAR' }
84
+ # MyRepository.new.send(:wrap, input) == MyEntity.wrap(input)
85
+ #
86
+ # @example custom coercion by symbol, for entity type than responds to described_method
87
+ # class MyRepository
88
+ # entity MyEntity, :build
89
+ # end
90
+ #
91
+ # input = { foo: 'FOO', bar: 'BAR' }
92
+ # MyRepository.new.send(:wrap, input) == MyEntity.build(input)
93
+ #
94
+ # @example custom coercion by callable object
95
+ # class MyRepository
96
+ # entity MyEntity, BUILD_ENTITY
97
+ # end
98
+ #
99
+ # input = { foo: 'FOO', bar: 'BAR' }
100
+ # MyRepository.new.send(:wrap, input) == BUILD_ENTITY(input)
101
+ def entity(entity, coercion = nil)
102
+ @entity_class = entity
103
+ @__entity_coercion__ = __build_entity_coercion__(coercion)
104
+ @entity_class
105
+ end
106
+
107
+ # Configure Mapper
108
+ #
109
+ # @example With anonymous mapper
110
+ # class Repository < LunaPark::Repository
111
+ # mapper do
112
+ # attr :foo, row: :fuu
113
+ # end
114
+ # end
115
+ #
116
+ # Repository.mapper_class.to_row(foo: 'Foo') # => { fuu: 'Foo' }
117
+ #
118
+ # @example With mapper class
119
+ # class Repository::Mapper < LunaPark::Mapper
120
+ # attr :foo, row: :fuu
121
+ # end
122
+ #
123
+ # class Repository < LunaPark::Repository
124
+ # mapper Mapper
125
+ # end
126
+ #
127
+ # Repository.new.mapper_class.to_row(foo: 'Foo') # => { fuu: 'Foo' }
128
+ #
129
+ # @example Without mapper
130
+ # class Repository < LunaPark::Repository
131
+ # def example_to_row(attrs)
132
+ # to_row attrs
133
+ # end
134
+ # end
135
+ #
136
+ # Repository.new.example_to_row(foo: 'Foo') # => { foo: 'Foo' }
137
+ #
138
+ def mapper(mapper = Undefined, &block)
139
+ raise ArgumentError, 'Expected mapper xOR block' unless (mapper == Undefined) ^ block.nil?
140
+
141
+ return @mapper_class = mapper if block.nil?
142
+
143
+ @mapper_class = Class.new(base_anonymous_mapper)
144
+ @mapper_class.class_eval(&block)
145
+ @mapper_class
146
+ end
147
+
148
+ def __build_entity_coercion__(coercion) # rubocop:disable Metrics/AbcSize
149
+ return entity_class.method(coercion) if coercion.is_a? Symbol
150
+ return coercion if coercion.respond_to?(:call)
151
+
152
+ raise ArgumentError, 'coercion MUST be call\'able, Symbol or nil' unless coercion.nil?
153
+
154
+ infer_entity_coercion
155
+ end
156
+
157
+ def infer_entity_coercion # rubocop:disable Metrics/AbcSize
158
+ return entity_class.method(:call) if entity_class.respond_to?(:call)
159
+ return entity_class.method(:wrap) if entity_class.respond_to?(:wrap)
160
+
161
+ ->(input) { entity_class.new(input.to_h) }
51
162
  end
52
163
 
53
- def mapper(mapper_class = nil)
54
- @mapper_class = mapper_class
164
+ # @abstract
165
+ #
166
+ # @example
167
+ # class Transaction::Repository < LunaPark::Repository
168
+ # # Parent of this mapper will be changed
169
+ # mapper do
170
+ # attr :foo
171
+ # end
172
+ #
173
+ # def self.base_anonymous_mapper
174
+ # MyBaseMapper
175
+ # end
176
+ # end
177
+ def base_anonymous_mapper
178
+ LunaPark::Mappers::Codirectional
179
+ end
180
+
181
+ def primary_key(attr)
182
+ @primary_key_attr = attr
55
183
  end
56
184
 
57
185
  DEFAULT_PRIMARY_KEY = :id
58
186
 
59
- def primary_key(pk = nil)
60
- @db_primary_key = pk
187
+ def primary_key_attr
188
+ @primary_key_attr || DEFAULT_PRIMARY_KEY
189
+ end
190
+
191
+ def __define_constants__(not_found: LunaPark::Extensions::DataMapper::NotFound)
192
+ __define_class__ 'NotFound', not_found
61
193
  end
62
194
 
63
- def db_primary_key
64
- @db_primary_key || DEFAULT_PRIMARY_KEY
195
+ def __define_class__(name, parent)
196
+ klass = Class.new(parent)
197
+ const_set name, klass
65
198
  end
199
+
200
+ def inherited(klass)
201
+ klass.__define_constants__(not_found: NotFound)
202
+ klass.entity entity_class, __entity_coercion__
203
+ klass.mapper mapper_class
204
+ super
205
+ end
206
+
207
+ class Undefined; end
208
+ private_constant :Undefined
66
209
  end
67
210
 
68
211
  module InstanceMethods
@@ -74,22 +217,56 @@ module LunaPark
74
217
 
75
218
  # Helpers
76
219
 
220
+ # Repository Helpers
221
+
77
222
  # Get collection of entities from row
78
223
  # @example
79
224
  # def where_type(type)
80
- # read_all products.where(type: type)
225
+ # read_all scoped dataset.where(type: type)
81
226
  # end
82
227
  def read_all(rows)
83
- to_entities from_rows rows.to_a
228
+ to_entities from_rows __to_array__(rows)
84
229
  end
85
230
 
86
231
  # Get one entity from row
87
232
  # @example
88
233
  # def find(id)
89
- # read_all products.where(id: id)
234
+ # # limit 2 allows to check if there are more than 1 record
235
+ # read_one dataset.where(id: id).limit(2)
236
+ # end
237
+ def read_one(rows)
238
+ to_entity from_row __one_from__(rows)
239
+ end
240
+
241
+ # Get one entity from row
242
+ # @example
243
+ # def find!(id)
244
+ # read_one! dataset.where(id: id).limit(1)
245
+ # end
246
+ def read_one!(row, not_found_by: nil, not_found_meta: nil)
247
+ warn 'Deprecated option #not_found_meta used' unless not_found_meta.nil?
248
+
249
+ found! read_one(row), not_found_by: not_found_by || not_found_meta
250
+ end
251
+
252
+ # Check if record was found
253
+ # @example
254
+ # class MyRepository < LunaPark::Repository
255
+ # def find_by_x!(x)
256
+ # found! nil, not_found_by: "x: #{x}"
257
+ # end
258
+ # end
259
+ #
260
+ # begin
261
+ # MyRepository.new.find_by_x 'X'
262
+ # rescue MyRepository::NotFound => e
263
+ # raise HTTP404, "Record #{e.details[:name]} not found by #{e.details[:by]}"
90
264
  # end
91
- def read_one(row)
92
- to_entity from_row row
265
+ #
266
+ def found!(value, not_found_by: nil)
267
+ return value unless value.nil?
268
+
269
+ raise self.class::NotFound.new name: self.class.entity_class.name, by: not_found_by
93
270
  end
94
271
 
95
272
  # Mapper helpers
@@ -100,7 +277,7 @@ module LunaPark
100
277
  # database.insert_many(rows)
101
278
  # end
102
279
  def to_rows(input_array)
103
- mapper_class ? mapper_class.to_rows(input_array) : input_array.map(&:to_h)
280
+ self.class.mapper_class.to_rows(input_array)
104
281
  end
105
282
 
106
283
  # @example
@@ -109,7 +286,7 @@ module LunaPark
109
286
  # database.insert(row)
110
287
  # end
111
288
  def to_row(input)
112
- mapper_class ? mapper_class.to_row(input) : input.to_h
289
+ self.class.mapper_class.to_row(input)
113
290
  end
114
291
 
115
292
  # @example
@@ -118,7 +295,7 @@ module LunaPark
118
295
  # entities_attrs.map { |entity_attrs| Entity.new(entity_attrs) }
119
296
  # end
120
297
  def from_rows(rows_array)
121
- mapper_class ? mapper_class.from_rows(rows_array) : rows_array
298
+ self.class.mapper_class.from_rows(rows_array)
122
299
  end
123
300
 
124
301
  # @example
@@ -130,7 +307,7 @@ module LunaPark
130
307
  return if input.nil?
131
308
  raise ArgumentError, 'Can not be an Array' if input.is_a?(Array)
132
309
 
133
- mapper_class ? mapper_class.from_row(input.to_h) : input
310
+ self.class.mapper_class.from_row(input.to_h)
134
311
  end
135
312
 
136
313
  # Entity construction helpers
@@ -139,7 +316,7 @@ module LunaPark
139
316
  # to_entities(attributes_hashes) # => Array of Entity
140
317
  # to_entities(attributes_hash) # => Array of Entity
141
318
  def to_entities(attrs_array)
142
- Array(attrs_array).map { |attrs| to_entity(attrs) }
319
+ __to_array__(attrs_array).map { |attrs| to_entity(attrs) }
143
320
  end
144
321
 
145
322
  # @example
@@ -147,48 +324,103 @@ module LunaPark
147
324
  def to_entity(attrs)
148
325
  return if attrs.nil?
149
326
 
150
- entity_class ? entity_class.new(attrs) : attrs
327
+ self.class.entity_class.new(attrs)
151
328
  end
152
329
 
153
330
  # Entity wrapping helpers
154
331
 
155
332
  # @example
156
- # to_entities(attributes_hashes) # => Array of Entity
157
- # to_entities(entities) # => Array of Entity
158
- # to_entities(entity) # => Array of Entity
333
+ # wrap_all(attributes_hashes) # => Array of Entity
334
+ # wrap_all(entities) # => Array of Entity
335
+ # wrap_all(entity) # => Array of Entity
159
336
  def wrap_all(input_array)
160
- Array(input_array).map { |input| wrap(input) }
337
+ __to_array__(input_array).map { |input| wrap(input) }
161
338
  end
162
339
 
163
340
  # @example
164
- # to_entity(attributes_hash) # => Entity
165
- # to_entity(entity) # => Entity
341
+ # wrap(id: 42) # => <#MyEntity @id=42>
342
+ # wrap(entity) # => <#MyEntity @id=42>
166
343
  def wrap(input)
167
344
  return if input.nil?
168
345
 
169
- entity_class ? entity_class.wrap(input) : input
346
+ self.class.__entity_coercion__.call(input)
170
347
  end
171
348
 
172
- # Read config
173
-
174
- def mapper_class
175
- self.class.mapper_class
349
+ # @example scope after query build
350
+ # def all(**opts)
351
+ # read_all scoped(**opts).order(:created_at)
352
+ # end
353
+ #
354
+ # @example scope before query build
355
+ # def all(**opts)
356
+ # read_all scoped(dataset.order(:created_at), **opts)
357
+ # end
358
+ #
359
+ def scoped(ds = dataset, **opts)
360
+ scope(ds, **opts)
176
361
  end
177
362
 
178
- def entity_class
179
- self.class.entity_class
363
+ # @abstract
364
+ #
365
+ # @example
366
+ # def scope(dataset, deleted: nil, for_update: false, **scope)
367
+ # ds = super(dataset, **scope)
368
+ # ds = ds.for_update if for_update == true
369
+ # ds = ds.where(deleted_at: nil) if deleted == false
370
+ # ds = ds.where.not(deleted_at: nil) if deleted == true
371
+ # ds
372
+ # end
373
+ #
374
+ # def all(**scope)
375
+ # read_all scoped(**scope) # same as `scope(dataset, **scope)`
376
+ # end
377
+ #
378
+ # all # get all
379
+ # all(deleted: false) # get not deleted
380
+ # all(deleted: true) # get deleted
381
+ def scope(dataset, **_scope)
382
+ dataset
180
383
  end
181
384
 
385
+ # Read config
386
+
182
387
  def primary_key
183
- self.class.db_primary_key
388
+ self.class.primary_key_attr
184
389
  end
185
390
 
186
391
  # Factory Methods
187
392
 
393
+ # @abstract
394
+ #
188
395
  # Usefull for extensions
189
396
  def dataset
190
397
  raise NotImplementedError
191
398
  end
399
+
400
+ # fixes problem: `Array({ a: 1 }) # => [[:a, 1]]`
401
+ def __to_array__(input)
402
+ input.is_a?(Hash) ? [input] : Array(input)
403
+ end
404
+
405
+ # checks if there are only one item in the given array
406
+ def __one_from__(input)
407
+ case input
408
+ when Hash then input
409
+ else
410
+ array = input.is_a?(Array) ? input : Array(input)
411
+ raise MoreThanOneRecord.new count: array.size if array.size > 1
412
+
413
+ array.first
414
+ end
415
+ end
416
+ end
417
+
418
+ class NotFound < LunaPark::Errors::NotFound
419
+ message { |d| "#{d[:name]} (#{d[:by]})" }
420
+ end
421
+
422
+ class MoreThanOneRecord < LunaPark::Errors::System
423
+ message { |d| "Expected only one record, but there are #{d[:count]} records" }
192
424
  end
193
425
  end
194
426
  end
@@ -7,9 +7,15 @@ module LunaPark
7
7
  module Create
8
8
  def create(input)
9
9
  entity = wrap(input)
10
- row = to_row(entity)
11
- new_row = dataset.returning.insert(row).first
10
+
11
+ time = Time.now
12
+ entity.created_at = time if entity.respond_to?(:created_at)
13
+ entity.updated_at = time if entity.respond_to?(:updated_at)
14
+
15
+ row = to_row(entity)
16
+ new_row = dataset.returning.insert(row).first
12
17
  new_attrs = from_row(new_row)
18
+
13
19
  entity.set_attributes(new_attrs)
14
20
  entity
15
21
  end
@@ -6,7 +6,7 @@ module LunaPark
6
6
  module Postgres
7
7
  module Delete
8
8
  def delete(uid)
9
- dataset.returning.where(primary_key => uid).delete
9
+ dataset.where(primary_key => uid).delete.positive?
10
10
  end
11
11
  end
12
12
  end
@@ -5,56 +5,47 @@ module LunaPark
5
5
  module Repositories
6
6
  module Postgres
7
7
  module Read
8
- def find!(pk_value, for_update: false)
9
- ds = dataset.where(primary_key => pk_value)
10
- read_one!(ds, for_update: for_update, not_found_meta: pk_value)
11
- end
12
-
13
- def find(pk_value, for_update: false)
14
- ds = dataset.where(primary_key => pk_value)
15
- read_one(ds, for_update: for_update)
16
- end
17
-
18
8
  def lock!(pk_value)
19
- lock(pk_value) || raise(Errors::NotFound, "#{short_class_name} (#{pk_value})")
9
+ transaction { yield find! pk_value, for_update: true }
20
10
  end
21
11
 
22
12
  def lock(pk_value)
23
- dataset.for_update.select(primary_key).where(primary_key => pk_value).first ? true : false
13
+ transaction { yield find pk_value, for_update: true }
24
14
  end
25
15
 
26
- def count
27
- dataset.count
16
+ def transaction(&block)
17
+ dataset.transaction(&block)
28
18
  end
29
19
 
30
- def all
31
- read_all(dataset.order { created_at.desc })
20
+ def find!(pk_value, **scope)
21
+ found! find(pk_value, **scope), not_found_by: pk_value
32
22
  end
33
23
 
34
- def last
35
- to_entity from_row dataset.order(:created_at).last
24
+ def find(pk_value, **scope)
25
+ read_one scoped(**scope).where(primary_key => pk_value)
36
26
  end
37
27
 
38
- private
28
+ def count(**scope)
29
+ scoped(**scope).count
30
+ end
39
31
 
40
- def read_one!(dataset, for_update: false, not_found_meta:)
41
- read_one(dataset, for_update: for_update).tap do |entity|
42
- raise Errors::NotFound, "#{short_class_name} (#{not_found_meta})" if entity.nil?
43
- end
32
+ def all(**scope)
33
+ read_all(scoped(**scope).order { created_at.desc })
44
34
  end
45
35
 
46
- def read_one(dataset, for_update: false)
47
- dataset = dataset.for_update if for_update
48
- row = dataset.first
49
- to_entity from_row(row)
36
+ def first(**scope)
37
+ read_one scoped(**scope).order(:created_at).first
50
38
  end
51
39
 
52
- def read_all(dataset)
53
- to_entities from_rows(dataset)
40
+ def last(**scope)
41
+ read_one scoped(**scope).order(:created_at).last
54
42
  end
55
43
 
56
- def short_class_name
57
- @short_class_name ||= self.class.name[/::(\w+)\z/, 1]
44
+ private
45
+
46
+ def scope(dataset, for_update: false, **_)
47
+ dataset = dataset.for_update if for_update
48
+ dataset
58
49
  end
59
50
  end
60
51
  end
@@ -7,10 +7,13 @@ module LunaPark
7
7
  module Update
8
8
  def save(input)
9
9
  entity = wrap(input)
10
- entity.updated_at = Time.now.utc
11
- row = to_row(entity)
12
- new_row = dataset.returning.where(primary_key => row[primary_key]).update(row).first
10
+
11
+ entity.updated_at = Time.now if entity.respond_to?(:updated_at)
12
+
13
+ row = to_row(entity)
14
+ new_row = dataset.where(primary_key => row[primary_key]).returning.update(row).first
13
15
  new_attrs = from_row(new_row)
16
+
14
17
  entity.set_attributes(new_attrs)
15
18
  entity
16
19
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/mappers/codirectional'
4
+
5
+ module LunaPark
6
+ class Mapper < Mappers::Codirectional; end
7
+ end
@@ -7,32 +7,46 @@ module LunaPark
7
7
  # Copyist for copiyng value between two schemas with DIFFERENT or NESTED paths
8
8
  # (Works with only one described attribute)
9
9
  class Nested
10
- def initialize(attrs_path:, row_path:)
10
+ def initialize(attrs_path:, row_path:, mapper:, map_array:)
11
11
  @attrs_path = attrs_path
12
12
  @row_path = row_path
13
13
 
14
+ @mapper_config = mapper
15
+ @map_array = map_array
16
+
14
17
  raise ArgumentError, 'attr path can not be nil' if attrs_path.nil?
15
18
  raise ArgumentError, 'store path can not be nil' if row_path.nil?
19
+ raise ArgumentError, 'array option MUST be nil when no Mapper given' if map_array && mapper.nil?
16
20
  end
17
21
 
18
22
  def from_row(row:, attrs:)
19
- copy_nested(from: row, to: attrs, from_path: @row_path, to_path: @attrs_path)
23
+ copy_nested(from: row, to: attrs, from_path: @row_path, to_path: @attrs_path, direction: :from_row)
20
24
  end
21
25
 
22
26
  def to_row(row:, attrs:)
23
- copy_nested(from: attrs, to: row, from_path: @attrs_path, to_path: @row_path)
27
+ copy_nested(from: attrs, to: row, from_path: @attrs_path, to_path: @row_path, direction: :to_row)
24
28
  end
25
29
 
26
30
  private
27
31
 
28
- def copy_nested(from:, to:, from_path:, to_path:)
32
+ def copy_nested(from:, to:, from_path:, to_path:, direction:)
29
33
  value = read(from, from_path)
30
34
 
31
35
  return if value == Undefined # omit undefined keys
32
36
 
37
+ value = apply_mapper(value, direction) unless mapper.nil?
38
+
33
39
  write(to, to_path, value)
34
40
  end
35
41
 
42
+ def apply_mapper(value, direction)
43
+ if @map_array
44
+ value.map { |v| mapper.public_send direction, v }
45
+ else
46
+ mapper.public_send direction, value
47
+ end
48
+ end
49
+
36
50
  def read(from, from_path)
37
51
  if from_path.is_a?(Array) # when given `%i[key path]` - not just `:key`
38
52
  read_nested(from, path: from_path)
@@ -82,6 +96,13 @@ module LunaPark
82
96
  path.inject(nested_hash) { |output, key| output[key] ||= {} }
83
97
  end
84
98
 
99
+ def mapper
100
+ return @mapper if instance_variable_defined?(:@mapper)
101
+ return @mapper = Object.const_get(@mapper_config) if @mapper_config.is_a? String
102
+
103
+ @mapper = @mapper_config
104
+ end
105
+
85
106
  class Undefined; end
86
107
 
87
108
  private_constant :Undefined
@@ -53,33 +53,28 @@ module LunaPark
53
53
  # class Mappers::Transaction < LunaPark::Mappers::Codirectional
54
54
  # attr :uid, row: :id
55
55
  # attr %i[charge amount], row: :charge_amount
56
+ # attr :comment
56
57
  # end
57
- def attr(attr, row: nil)
58
- return attrs(attr) if row.nil?
59
-
58
+ def attr(attr, row: attr, mapper: nil, array: nil)
60
59
  attr_path = to_path(attr)
61
60
  row_path = to_path(row)
62
61
 
63
- if attr_path == row_path
64
- attrs(attr_path)
62
+ if attr_path == row_path && !attr_path.is_a?(Array)
63
+ slice_copyist.add_key(attr_path)
65
64
  else
66
- nested_copyists << Copyists::Nested.new(attrs_path: attr_path, row_path: row_path)
65
+ nested_copyists << Copyists::Nested.new(
66
+ attrs_path: attr_path, row_path: row_path,
67
+ mapper: mapper, map_array: array
68
+ )
67
69
  end
68
70
  end
69
71
 
70
72
  # @example
71
73
  # class Mappers::Transaction < LunaPark::Mappers::Codirectional
72
- # attrs :comment, :uid, %i[addresses home], :created_at
74
+ # attrs :created_at, :updated_at, :deleted_at
73
75
  # end
74
76
  def attrs(*common_keys)
75
- common_keys.each do |common_key|
76
- path = to_path(common_key)
77
- if path.is_a?(Array)
78
- nested_copyists << Copyists::Nested.new(attrs_path: path, row_path: path)
79
- else
80
- slice_copyist.add_key(path)
81
- end
82
- end
77
+ common_keys.each { |common_key| attr common_key }
83
78
  end
84
79
 
85
80
  def from_row(input)
@@ -6,7 +6,7 @@ module LunaPark
6
6
  module Mappers
7
7
  module Errors
8
8
  class NotArray < LunaPark::Errors::System
9
- message { |d| "input MUST be an Array, but given #{d[:input].class} `#{d[:input].inspect}`" }
9
+ message { |d| "input MUST respond to #to_a, but given #{d[:input].class} `#{d[:input].inspect}`" }
10
10
  end
11
11
  end
12
12
  end
@@ -67,28 +67,30 @@ module LunaPark
67
67
  # Transforms array of rows to array of attribute hashes
68
68
  def from_rows(rows)
69
69
  return [] if rows.nil?
70
- raise Errors::NotArray.new(input: rows) unless rows.is_a?(Array)
70
+ raise Errors::NotArray.new(input: rows) if rows.is_a?(Hash)
71
+ raise Errors::NotArray.new(input: rows) unless rows.respond_to?(:to_a)
71
72
 
72
73
  rows.to_a.map { |hash| from_row(hash) }
73
74
  end
74
75
 
75
76
  ##
76
77
  # Transforms array of attribute hashes to array of rows
77
- def to_rows(attr_hashes)
78
- return [] if attr_hashes.nil?
79
- raise Errors::NotArray.new(input: attr_hashes) unless attr_hashes.is_a?(Array)
78
+ def to_rows(attrs_array)
79
+ return [] if attrs_array.nil?
80
+ raise Errors::NotArray.new(input: rows) if attrs_array.is_a?(Hash)
81
+ raise Errors::NotArray.new(input: attrs_array) unless attrs_array.respond_to?(:to_a)
80
82
 
81
- attr_hashes.to_a.map { |entity| to_row(entity) }
83
+ attrs_array.to_a.map { |entity| to_row(entity) }
82
84
  end
83
85
 
84
86
  # @abstract
85
- def from_row(_row)
86
- raise LunaPark::Errors::AbstractMethod
87
+ def from_row(row)
88
+ row.to_h
87
89
  end
88
90
 
89
91
  # @abstract
90
- def to_row(_attrs)
91
- raise LunaPark::Errors::AbstractMethod
92
+ def to_row(attrs)
93
+ attrs.to_h
92
94
  end
93
95
  end
94
96
  end
@@ -48,17 +48,17 @@ module LunaPark
48
48
  end
49
49
  end
50
50
 
51
- def error_payload(e)
51
+ def error_payload(err) # rubocop:disable Metrics/MethodLength
52
52
  error_hash = {
53
- class: e.class,
54
- message: e.message
53
+ class: err.class,
54
+ message: err.message
55
55
  }
56
- error_hash.merge!(backtrace: "\n" + e.backtrace.join("\n") + "\n") if e.backtrace
56
+ error_hash.merge!(backtrace: "\n" + err.backtrace.join("\n") + "\n") if err.backtrace
57
57
  payload = {
58
58
  error: error_hash,
59
59
  ok: false
60
60
  }
61
- payload.merge!(details: e.details) if e.respond_to?(:details)
61
+ payload.merge!(details: err.details) if err.respond_to?(:details)
62
62
  payload
63
63
  end
64
64
 
@@ -40,7 +40,7 @@ module LunaPark
40
40
 
41
41
  def push_tags(*tags)
42
42
  tags.flatten!
43
- tags.reject! { |t| t.respond_to?(:empty?) ? !!t.empty? : !t }
43
+ tags.reject! { |tag| tag.respond_to?(:empty?) ? tag.empty? : !tag }
44
44
  current_tags.concat tags
45
45
  tags
46
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LunaPark
4
- VERSION = '0.12.1'
4
+ VERSION = '0.13.0'
5
5
  end
data/lib/luna_park.rb CHANGED
@@ -66,8 +66,7 @@ LunaPark::Tools.if_gem_installed('dry-validation', '~> 1.1') { require 'luna_par
66
66
  require 'luna_park/values/compound'
67
67
  require 'luna_park/values/single'
68
68
  require 'luna_park/values/attributable'
69
- require 'luna_park/mappers/simple'
70
- require 'luna_park/mappers/codirectional'
69
+ require 'luna_park/mapper'
71
70
  require 'luna_park/repository'
72
71
  require 'luna_park/repositories/sequel'
73
72
  require 'luna_park/repositories/postgres'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: luna_park
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Kudrin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-02-28 00:00:00.000000000 Z
12
+ date: 2023-12-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bugsnag
@@ -424,6 +424,7 @@ files:
424
424
  - lib/luna_park/http/request.rb
425
425
  - lib/luna_park/http/response.rb
426
426
  - lib/luna_park/http/send.rb
427
+ - lib/luna_park/mapper.rb
427
428
  - lib/luna_park/mappers/codirectional.rb
428
429
  - lib/luna_park/mappers/codirectional/copyists/nested.rb
429
430
  - lib/luna_park/mappers/codirectional/copyists/slice.rb