luna_park 0.12.1 → 0.13.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
  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