rom-repository 1.3.2 → 1.3.3
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 +4 -4
- data/.travis.yml +0 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile +2 -2
- data/README.md +1 -25
- data/lib/rom/repository.rb +12 -2
- data/lib/rom/repository/changeset/pipe.rb +43 -13
- data/lib/rom/repository/changeset/stateful.rb +42 -7
- data/lib/rom/repository/changeset/update.rb +3 -12
- data/lib/rom/repository/header_builder.rb +7 -3
- data/lib/rom/repository/mapper_builder.rb +2 -2
- data/lib/rom/repository/relation_proxy.rb +2 -0
- data/lib/rom/repository/relation_proxy/combine.rb +8 -2
- data/lib/rom/repository/struct_builder.rb +8 -2
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/struct.rb +36 -13
- data/rom-repository.gemspec +2 -2
- data/spec/integration/changeset_spec.rb +32 -2
- data/spec/integration/repository/aggregate_spec.rb +9 -0
- data/spec/integration/repository_spec.rb +39 -11
- data/spec/shared/relations.rb +1 -0
- data/spec/unit/changeset_spec.rb +8 -0
- data/spec/unit/relation_proxy_spec.rb +7 -0
- data/spec/unit/struct_builder_spec.rb +82 -52
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2939fe9e61cbec70da58fe863b4c49384feabf57
|
4
|
+
data.tar.gz: 7cf8ca8cfc9acb006d2065bcdfcd6fcac6c4531e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ee593244830ea931207b4acd726c9475543e6f8dd1bf65a3741c51f75bc1eee9fee974509fc4298005e2958500c38e2e313ed639b63f564de178cb4eb5bdb08
|
7
|
+
data.tar.gz: e3c05763d9d382a268ee5d29af29472826c86ac538b3b4364b80a36379ecb5d822aa5ced697fb836a111a212c700c24694c13f73f6acd0de6c04dcdd5dac145f
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
# v1.3.3 2017-05-31
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* `Changeset#extend` to exclude steps from the `#diff` output, this allows
|
6
|
+
to filter out timestamp changes prior to updates so that we can avoid
|
7
|
+
hitting the database in case of timestamp-only changes. You still can call `.map(:touch)`
|
8
|
+
if you want to have `updated_at` refreshed unconditionally (flash-gordon)
|
9
|
+
|
10
|
+
## Fixed
|
11
|
+
|
12
|
+
* `aggregate` and `combine` works correctly with nested graph options where associations are aliased (solnic)
|
13
|
+
* Auto-mapping no longer creates intermediate struct objects for combined relations (which caused massive performance degradation in some cases) (solnic)
|
14
|
+
* Aliased associations no longer cause mapping to intermediate structs (solnic)
|
15
|
+
|
16
|
+
[Compare v1.3.2...v1.3.3](https://github.com/rom-rb/rom-repository/compare/v1.3.2...v1.3.3)
|
17
|
+
|
1
18
|
# v1.3.2 2017-05-02
|
2
19
|
|
3
20
|
### Fixed
|
data/Gemfile
CHANGED
@@ -11,8 +11,8 @@ group :development do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
group :test do
|
14
|
-
gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: '
|
15
|
-
gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: '
|
14
|
+
gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'release-3.0'
|
15
|
+
gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: 'release-1.0'
|
16
16
|
gem 'rspec'
|
17
17
|
gem 'dry-struct'
|
18
18
|
gem 'byebug', platforms: :mri
|
data/README.md
CHANGED
@@ -1,25 +1 @@
|
|
1
|
-
[
|
2
|
-
[travis]: https://travis-ci.org/rom-rb/rom-repository
|
3
|
-
[gemnasium]: https://gemnasium.com/rom-rb/rom-repository
|
4
|
-
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-repository
|
5
|
-
[inchpages]: http://inch-ci.org/github/rom-rb/rom-repository
|
6
|
-
|
7
|
-
# rom-repository
|
8
|
-
|
9
|
-
[][gem]
|
10
|
-
[][travis]
|
11
|
-
[][gemnasium]
|
12
|
-
[][codeclimate]
|
13
|
-
[][codeclimate]
|
14
|
-
[][inchpages]
|
15
|
-
|
16
|
-
Repositories for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping, changesets and commands.
|
17
|
-
|
18
|
-
Resources:
|
19
|
-
|
20
|
-
* [User documentation](http://rom-rb.org/learn/repositories)
|
21
|
-
* [API documentation](http://rubydoc.info/gems/rom-repository)
|
22
|
-
|
23
|
-
## License
|
24
|
-
|
25
|
-
See `LICENSE` file.
|
1
|
+
# This project was moved to [rom-rb/rom](https://github.com/rom-rb/rom/tree/master/repository)
|
data/lib/rom/repository.rb
CHANGED
@@ -82,10 +82,20 @@ module ROM
|
|
82
82
|
|
83
83
|
auto_struct true
|
84
84
|
|
85
|
+
# @!method self.auto_struct
|
86
|
+
# Get or set struct namespace
|
87
|
+
defines :struct_namespace
|
88
|
+
|
89
|
+
struct_namespace ROM::Struct
|
90
|
+
|
85
91
|
# @!attribute [r] container
|
86
92
|
# @return [ROM::Container] The container used to set up a repo
|
87
93
|
param :container, allow: ROM::Container
|
88
94
|
|
95
|
+
# @!attribute [r] struct_namespace
|
96
|
+
# @return [Module,Class] The namespace for auto-generated structs
|
97
|
+
option :struct_namespace, default: -> { self.class.struct_namespace }
|
98
|
+
|
89
99
|
# @!attribute [r] auto_struct
|
90
100
|
# @return [Boolean] The container used to set up a repo
|
91
101
|
option :auto_struct, default: -> { self.class.auto_struct }
|
@@ -111,7 +121,7 @@ module ROM
|
|
111
121
|
def initialize(container, opts = EMPTY_HASH)
|
112
122
|
super
|
113
123
|
|
114
|
-
@mappers = MapperBuilder.new
|
124
|
+
@mappers = MapperBuilder.new(struct_namespace: struct_namespace)
|
115
125
|
|
116
126
|
@relations = RelationRegistry.new do |registry, relations|
|
117
127
|
self.class.relations.each do |name|
|
@@ -272,7 +282,7 @@ module ROM
|
|
272
282
|
# end
|
273
283
|
#
|
274
284
|
# user
|
275
|
-
# # => #<ROM::Struct
|
285
|
+
# # => #<ROM::Struct::User id=1 name="Jane">
|
276
286
|
#
|
277
287
|
# @example with a rollback
|
278
288
|
# user = transaction do |t|
|
@@ -25,11 +25,11 @@ module ROM
|
|
25
25
|
#
|
26
26
|
# @api private
|
27
27
|
class Pipe < Transproc::Transformer[PipeRegistry]
|
28
|
-
|
28
|
+
extend Initializer
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
param :processor, default: -> { self.class.transproc }
|
31
|
+
option :use_for_diff, optional: true, default: -> { true }
|
32
|
+
option :diff_processor, optional: true, default: -> { use_for_diff ? processor : nil }
|
33
33
|
|
34
34
|
def self.[](name)
|
35
35
|
container[name]
|
@@ -40,20 +40,22 @@ module ROM
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def bind(context)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
43
|
+
return self unless processor.is_a?(Proc) || diff_processor.is_a?(Proc)
|
44
|
+
|
45
|
+
new(bind_processor(processor, context), diff_processor: bind_processor(diff_processor, context))
|
48
46
|
end
|
49
47
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
48
|
+
def compose(other, use_for_diff: other.is_a?(Pipe) ? other.use_for_diff : false)
|
49
|
+
new_proc = processor ? processor >> other : other
|
50
|
+
|
51
|
+
if use_for_diff
|
52
|
+
diff_proc = diff_processor ? diff_processor >> other : other
|
53
|
+
new(new_proc, diff_processor: diff_proc)
|
53
54
|
else
|
54
|
-
|
55
|
+
new(new_proc)
|
55
56
|
end
|
56
57
|
end
|
58
|
+
alias_method :>>, :compose
|
57
59
|
|
58
60
|
def call(data)
|
59
61
|
if processor
|
@@ -62,6 +64,34 @@ module ROM
|
|
62
64
|
data
|
63
65
|
end
|
64
66
|
end
|
67
|
+
|
68
|
+
def for_diff(data)
|
69
|
+
if diff_processor
|
70
|
+
diff_processor.call(data)
|
71
|
+
else
|
72
|
+
data
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def with(opts)
|
77
|
+
if opts.empty?
|
78
|
+
self
|
79
|
+
else
|
80
|
+
Pipe.new(processor, options.merge(opts))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def new(processor, opts = EMPTY_HASH)
|
85
|
+
Pipe.new(processor, options.merge(opts))
|
86
|
+
end
|
87
|
+
|
88
|
+
def bind_processor(processor, context)
|
89
|
+
if processor.is_a?(Proc)
|
90
|
+
self[-> *args { context.instance_exec(*args, &processor) }]
|
91
|
+
else
|
92
|
+
processor
|
93
|
+
end
|
94
|
+
end
|
65
95
|
end
|
66
96
|
end
|
67
97
|
end
|
@@ -55,11 +55,27 @@ module ROM
|
|
55
55
|
# @see https://github.com/solnic/transproc Transproc
|
56
56
|
#
|
57
57
|
# @api public
|
58
|
-
def self.map(&block)
|
58
|
+
def self.map(options = EMPTY_HASH, &block)
|
59
59
|
if block.parameters.empty?
|
60
|
-
pipes << Class.new(Pipe, &block).new
|
60
|
+
pipes << Class.new(Pipe, &block).new(options)
|
61
61
|
else
|
62
|
-
pipes << Pipe.new(block)
|
62
|
+
pipes << Pipe.new(block, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Define a changeset mapping excluded from diffs
|
67
|
+
#
|
68
|
+
# @see Changeset::Stateful.map
|
69
|
+
# @see Changeset::Stateful#extend
|
70
|
+
#
|
71
|
+
# @return [Array<Pipe>, Transproc::Function>]
|
72
|
+
#
|
73
|
+
# @api public
|
74
|
+
def self.extend(*, &block)
|
75
|
+
if block
|
76
|
+
map(use_for_diff: false, &block)
|
77
|
+
else
|
78
|
+
super
|
63
79
|
end
|
64
80
|
end
|
65
81
|
|
@@ -104,7 +120,7 @@ module ROM
|
|
104
120
|
# Apply mapping using built-in transformations and a custom block
|
105
121
|
#
|
106
122
|
# @example
|
107
|
-
# changeset.map(:
|
123
|
+
# changeset.map(:add_timestamps) { |tuple| tuple.merge(status: 'published') }
|
108
124
|
#
|
109
125
|
# @param [Array<Symbol>] steps A list of mapping steps
|
110
126
|
#
|
@@ -112,14 +128,33 @@ module ROM
|
|
112
128
|
#
|
113
129
|
# @api public
|
114
130
|
def map(*steps, &block)
|
131
|
+
extend(*steps, use_for_diff: true, &block)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Pipe changeset's data using custom steps define on the pipe.
|
135
|
+
# You should use #map instead except updating timestamp fields.
|
136
|
+
# Calling changeset.extend builds a pipe that excludes certain
|
137
|
+
# steps for generating the diff. Currently the only place where
|
138
|
+
# it is used is update changesets with the `:touch` step, i.e.
|
139
|
+
# `changeset.extend(:touch).diff` will exclude `:updated_at`
|
140
|
+
# from the diff.
|
141
|
+
#
|
142
|
+
# @see Changeset::Stateful#map
|
143
|
+
#
|
144
|
+
# @return [Changeset]
|
145
|
+
#
|
146
|
+
# @api public
|
147
|
+
def extend(*steps, use_for_diff: false, **opts, &block)
|
148
|
+
options = { use_for_diff: use_for_diff, **opts }
|
149
|
+
|
115
150
|
if block
|
116
151
|
if steps.size > 0
|
117
|
-
|
152
|
+
extend(*steps, options).extend(options, &block)
|
118
153
|
else
|
119
|
-
with(pipe: pipe
|
154
|
+
with(pipe: pipe.compose(Pipe.new(block).bind(self), options))
|
120
155
|
end
|
121
156
|
else
|
122
|
-
with(pipe: steps.reduce(pipe) { |a, e| a
|
157
|
+
with(pipe: steps.reduce(pipe.with(options)) { |a, e| a.compose(pipe[e], options) })
|
123
158
|
end
|
124
159
|
end
|
125
160
|
|
@@ -41,16 +41,6 @@ module ROM
|
|
41
41
|
@original ||= Hash(relation.one)
|
42
42
|
end
|
43
43
|
|
44
|
-
# Return diff hash sent through the pipe
|
45
|
-
#
|
46
|
-
# @return [Hash]
|
47
|
-
#
|
48
|
-
# @api public
|
49
|
-
def to_h
|
50
|
-
pipe.call(diff)
|
51
|
-
end
|
52
|
-
alias_method :to_hash, :to_h
|
53
|
-
|
54
44
|
# Return true if there's a diff between original and changeset data
|
55
45
|
#
|
56
46
|
# @return [TrueClass, FalseClass]
|
@@ -77,8 +67,9 @@ module ROM
|
|
77
67
|
def diff
|
78
68
|
@diff ||=
|
79
69
|
begin
|
80
|
-
|
81
|
-
|
70
|
+
data = pipe.for_diff(__data__)
|
71
|
+
data_tuple = data.to_a
|
72
|
+
data_keys = data.keys & original.keys
|
82
73
|
|
83
74
|
new_tuple = data_tuple.to_a.select { |(k, _)| data_keys.include?(k) }
|
84
75
|
ori_tuple = original.to_a.select { |(k, _)| data_keys.include?(k) }
|
@@ -7,8 +7,8 @@ module ROM
|
|
7
7
|
class HeaderBuilder
|
8
8
|
attr_reader :struct_builder
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@struct_builder = StructBuilder.new
|
10
|
+
def initialize(struct_namespace: nil, **options)
|
11
|
+
@struct_builder = StructBuilder.new(struct_namespace)
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(ast)
|
@@ -28,7 +28,11 @@ module ROM
|
|
28
28
|
name = meta[:combine_name] || relation_name
|
29
29
|
|
30
30
|
model = meta.fetch(:model) do
|
31
|
-
|
31
|
+
if meta[:combine_name]
|
32
|
+
false
|
33
|
+
else
|
34
|
+
struct_builder[name, header]
|
35
|
+
end
|
32
36
|
end
|
33
37
|
|
34
38
|
options = [visit(header), model: model]
|
@@ -73,7 +73,13 @@ module ROM
|
|
73
73
|
end
|
74
74
|
else
|
75
75
|
if value.is_a?(Array)
|
76
|
-
|
76
|
+
other =
|
77
|
+
if registry.key?(key)
|
78
|
+
registry[key]
|
79
|
+
else
|
80
|
+
registry[associations[key].target]
|
81
|
+
end
|
82
|
+
curried = combine_from_assoc(key, other).combine(*value)
|
77
83
|
result, _, keys = combine_opts_for_assoc(key)
|
78
84
|
combine_opts[result][key] = [curried, keys]
|
79
85
|
else
|
@@ -89,7 +95,7 @@ module ROM
|
|
89
95
|
}
|
90
96
|
end
|
91
97
|
|
92
|
-
__new__(relation.
|
98
|
+
__new__(relation.graph(*nodes))
|
93
99
|
end
|
94
100
|
|
95
101
|
# Shortcut for combining with parents which infers the join keys
|
@@ -30,6 +30,12 @@ module ROM
|
|
30
30
|
end
|
31
31
|
alias_method :[], :call
|
32
32
|
|
33
|
+
attr_reader :namespace
|
34
|
+
|
35
|
+
def initialize(namespace = nil)
|
36
|
+
@namespace = namespace || ROM::Struct
|
37
|
+
end
|
38
|
+
|
33
39
|
private
|
34
40
|
|
35
41
|
def visit(ast)
|
@@ -66,11 +72,11 @@ module ROM
|
|
66
72
|
end
|
67
73
|
|
68
74
|
def build_class(name, parent, &block)
|
69
|
-
Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent).call(&block)
|
75
|
+
Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent, namespace: namespace).call(&block)
|
70
76
|
end
|
71
77
|
|
72
78
|
def class_name(name)
|
73
|
-
|
79
|
+
Dry::Core::Inflector.classify(Dry::Core::Inflector.singularize(name))
|
74
80
|
end
|
75
81
|
end
|
76
82
|
end
|
data/lib/rom/struct.rb
CHANGED
@@ -7,14 +7,21 @@ module ROM
|
|
7
7
|
# They implement Hash protocol which means that they can be used
|
8
8
|
# in places where Hash-like objects are supported.
|
9
9
|
#
|
10
|
-
# Repositories define subclasses of ROM::Struct automatically, they are
|
11
|
-
# defined
|
12
|
-
# to use
|
10
|
+
# Repositories define subclasses of ROM::Struct automatically, they are
|
11
|
+
# defined in the ROM::Struct namespace by default, but you set it up
|
12
|
+
# to use your namespace/module as well.
|
13
13
|
#
|
14
14
|
# Structs are based on dry-struct gem, they include `schema` with detailed information
|
15
15
|
# about attribute types returned from relations, thus can be introspected to build
|
16
16
|
# additional functionality when desired.
|
17
17
|
#
|
18
|
+
# There is a caveat you should know about when working with structs. Struct classes
|
19
|
+
# have names but at the same time they're anonymous, i.e. you can't get the User struct class
|
20
|
+
# with ROM::Struct::User. ROM will create as many struct classes for User as needed,
|
21
|
+
# they all will have the same name and ROM::Struct::User will be the common parent class for
|
22
|
+
# them. Combined with the ability to provide your own namespace for structs this enables to
|
23
|
+
# pre-define the parent class.
|
24
|
+
#
|
18
25
|
# @example accessing relation struct model
|
19
26
|
# rom = ROM.container(:sql, 'sqlite::memory') do |conf|
|
20
27
|
# conf.default.create_table(:users) do
|
@@ -30,7 +37,7 @@ module ROM
|
|
30
37
|
#
|
31
38
|
# # get auto-generated User struct
|
32
39
|
# model = user_repo.users.mapper.model
|
33
|
-
# # => ROM::Struct
|
40
|
+
# # => ROM::Struct::User
|
34
41
|
#
|
35
42
|
# # see struct's schema attributes
|
36
43
|
#
|
@@ -40,11 +47,31 @@ module ROM
|
|
40
47
|
# model.schema[:name]
|
41
48
|
# # => #<Dry::Types::Sum left=#<Dry::Types::Constrained type=#<Dry::Types::Definition primitive=NilClass options={}> options={:rule=>#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[NilClass]}>} rule=#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[NilClass]}>> right=#<Dry::Types::Definition primitive=String options={}> options={:meta=>{:name=>:name, :source=>ROM::Relation::Name(users)}}>
|
42
49
|
#
|
50
|
+
# @example passing a namespace with an existing parent class
|
51
|
+
# module Entities
|
52
|
+
# class User < ROM::Struct
|
53
|
+
# def upcased_name
|
54
|
+
# name.upcase
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class UserRepo < ROM::Repository[:users]
|
60
|
+
# struct_namespace Entities
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# user_repo = UserRepo.new(rom)
|
64
|
+
# user = user_repo.users.by_pk(1).one!
|
65
|
+
# user.name # => "Jane"
|
66
|
+
# user.upcased_name # => "JANE"
|
67
|
+
#
|
43
68
|
# @see http://dry-rb.org/gems/dry-struct dry-struct
|
44
69
|
# @see http://dry-rb.org/gems/dry-types dry-types
|
45
70
|
#
|
46
71
|
# @api public
|
47
72
|
class Struct < Dry::Struct
|
73
|
+
MissingAttribute = Class.new(NameError)
|
74
|
+
|
48
75
|
# Returns a short string representation
|
49
76
|
#
|
50
77
|
# @return [String]
|
@@ -65,15 +92,11 @@ module ROM
|
|
65
92
|
|
66
93
|
private
|
67
94
|
|
68
|
-
def method_missing(
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
# see func name_err_mesg_to_str in error.c
|
74
|
-
name = inspected.size > 65 ? to_s : inspected
|
75
|
-
|
76
|
-
raise NoMethodError.new("undefined method `#{ m }' for #{ name }", m, args).tap { |e| e.set_backtrace(trace) }
|
95
|
+
def method_missing(method, *)
|
96
|
+
super
|
97
|
+
rescue NameError => error
|
98
|
+
raise if method == :to_ary
|
99
|
+
raise MissingAttribute.new("#{ error.message } (not loaded attribute?)")
|
77
100
|
end
|
78
101
|
end
|
79
102
|
end
|
data/rom-repository.gemspec
CHANGED
@@ -13,9 +13,9 @@ Gem::Specification.new do |gem|
|
|
13
13
|
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
14
14
|
gem.license = 'MIT'
|
15
15
|
|
16
|
-
gem.add_runtime_dependency 'rom', '~> 3.2'
|
16
|
+
gem.add_runtime_dependency 'rom', '~> 3.2', '>= 3.2.3'
|
17
17
|
gem.add_runtime_dependency 'rom-mapper', '~> 0.5'
|
18
|
-
gem.add_runtime_dependency 'dry-core', '~> 0.
|
18
|
+
gem.add_runtime_dependency 'dry-core', '~> 0.3', '>= 0.3.1'
|
19
19
|
gem.add_runtime_dependency 'dry-struct', '~> 0.1'
|
20
20
|
|
21
21
|
gem.add_development_dependency 'rake', '~> 11.2'
|
@@ -88,7 +88,7 @@ RSpec.describe 'Using changesets' do
|
|
88
88
|
it 'preprocesses data using built-in steps and custom block' do
|
89
89
|
changeset = repo.
|
90
90
|
changeset(:books, title: "rom-rb is awesome").
|
91
|
-
|
91
|
+
extend(:touch) { |tuple| tuple.merge(created_at: Time.now) }
|
92
92
|
|
93
93
|
command = repo.command(:create, repo.books)
|
94
94
|
result = command.(changeset)
|
@@ -134,7 +134,7 @@ RSpec.describe 'Using changesets' do
|
|
134
134
|
|
135
135
|
changeset = repo
|
136
136
|
.changeset(book.id, title: 'rom-rb is awesome for real')
|
137
|
-
.
|
137
|
+
.extend(:touch)
|
138
138
|
|
139
139
|
expect(changeset.diff).to eql(title: 'rom-rb is awesome for real')
|
140
140
|
|
@@ -150,6 +150,7 @@ RSpec.describe 'Using changesets' do
|
|
150
150
|
|
151
151
|
changeset = repo
|
152
152
|
.changeset(book.id, title: 'rom-rb is awesome')
|
153
|
+
.extend(:touch)
|
153
154
|
|
154
155
|
expect(changeset).to_not be_diff
|
155
156
|
|
@@ -159,5 +160,34 @@ RSpec.describe 'Using changesets' do
|
|
159
160
|
expect(result.title).to eql('rom-rb is awesome')
|
160
161
|
expect(result.updated_at).to be(nil)
|
161
162
|
end
|
163
|
+
|
164
|
+
it 'works with mixed several class-level pipes' do
|
165
|
+
book = repo.create(title: 'rom-rb is awesome')
|
166
|
+
|
167
|
+
changeset_class = Class.new(ROM::Changeset::Update[:books]) do
|
168
|
+
map { |title: | { title: title.upcase } }
|
169
|
+
extend { |title: | { title: title.reverse } }
|
170
|
+
end
|
171
|
+
|
172
|
+
changeset = repo
|
173
|
+
.changeset(changeset_class)
|
174
|
+
.by_pk(book.id)
|
175
|
+
.data(title: 'rom-rb is really awesome')
|
176
|
+
|
177
|
+
expect(changeset.diff).to eql(title: 'ROM-RB IS REALLY AWESOME')
|
178
|
+
expect(changeset.to_h).to eql(title: 'EMOSEWA YLLAER SI BR-MOR')
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'works with mixed several instance-level pipes' do
|
182
|
+
book = repo.create(title: 'rom-rb is awesome')
|
183
|
+
|
184
|
+
changeset = repo.
|
185
|
+
changeset(book.id, title: 'rom-rb is really awesome').
|
186
|
+
map { |title: | { title: title.upcase } }.
|
187
|
+
extend { |title: | { title: title.reverse } }
|
188
|
+
|
189
|
+
expect(changeset.diff).to eql(title: 'ROM-RB IS REALLY AWESOME')
|
190
|
+
expect(changeset.to_h).to eql(title: 'EMOSEWA YLLAER SI BR-MOR')
|
191
|
+
end
|
162
192
|
end
|
163
193
|
end
|
@@ -9,6 +9,15 @@ RSpec.describe ROM::Repository::Root, '#aggregate' do
|
|
9
9
|
include_context 'relations'
|
10
10
|
include_context 'seeds'
|
11
11
|
|
12
|
+
it 'loads a graph with aliased children and its parents' do
|
13
|
+
user = repo.aggregate(aliased_posts: :author).first
|
14
|
+
|
15
|
+
expect(user.aliased_posts.count).to be(1)
|
16
|
+
expect(user.aliased_posts[0].author.id).to be(user.id)
|
17
|
+
expect(user.aliased_posts[0].author.name).to eql(user.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
12
21
|
it 'exposes nodes via `node` method' do
|
13
22
|
jane = repo.
|
14
23
|
aggregate(:posts).
|
@@ -238,17 +238,45 @@ RSpec.describe 'ROM repository' do
|
|
238
238
|
end
|
239
239
|
|
240
240
|
describe 'projecting virtual attributes' do
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
241
|
+
before do
|
242
|
+
ROM::Repository::StructBuilder.cache.clear
|
243
|
+
ROM::Repository::MapperBuilder.cache.clear
|
244
|
+
end
|
245
|
+
|
246
|
+
shared_context 'auto-mapping' do
|
247
|
+
it 'loads auto-mapped structs' do
|
248
|
+
user = repo.users.
|
249
|
+
inner_join(:posts, author_id: :id).
|
250
|
+
select_group { [id.qualified, name.qualified] }.
|
251
|
+
select_append { int::count(:posts).as(:post_count) }.
|
252
|
+
having { count(id.qualified) >= 1 }.
|
253
|
+
first
|
254
|
+
|
255
|
+
expect(user.id).to be(1)
|
256
|
+
expect(user.name).to eql('Jane')
|
257
|
+
expect(user.post_count).to be(1)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context 'with default namespace' do
|
262
|
+
include_context 'auto-mapping'
|
263
|
+
end
|
264
|
+
|
265
|
+
context 'with custom struct namespace' do
|
266
|
+
before do
|
267
|
+
repo_class.struct_namespace(Test)
|
268
|
+
end
|
269
|
+
|
270
|
+
include_context 'auto-mapping'
|
271
|
+
|
272
|
+
it 'uses custom namespace' do
|
273
|
+
expect(Test.const_defined?(:User)).to be(false)
|
274
|
+
user = repo.users.limit(1).one!
|
275
|
+
|
276
|
+
expect(user.name).to eql('Jane')
|
277
|
+
expect(user.class).to be < Test::User
|
278
|
+
expect(user.class.name).to eql(Test::User.name)
|
279
|
+
end
|
252
280
|
end
|
253
281
|
end
|
254
282
|
|
data/spec/shared/relations.rb
CHANGED
data/spec/unit/changeset_spec.rb
CHANGED
@@ -66,6 +66,14 @@ RSpec.describe ROM::Changeset do
|
|
66
66
|
|
67
67
|
expect(changeset).to_not be_diff
|
68
68
|
end
|
69
|
+
|
70
|
+
it 'uses piped data for diff' do
|
71
|
+
expect(relation).to receive(:one).and_return(jane)
|
72
|
+
|
73
|
+
changeset = ROM::Changeset::Update.new(relation).data(name: "Jane").map { |name: | { name: name.upcase } }
|
74
|
+
|
75
|
+
expect(changeset).to be_diff
|
76
|
+
end
|
69
77
|
end
|
70
78
|
|
71
79
|
describe '#clean?' do
|
@@ -191,5 +191,12 @@ RSpec.describe 'loading proxy' do
|
|
191
191
|
it 'raises when method is missing' do
|
192
192
|
expect { users_proxy.not_here }.to raise_error(NoMethodError, "undefined method `not_here' for ROM::Relation[Users]")
|
193
193
|
end
|
194
|
+
|
195
|
+
it 'proxies Kernel methods when using with SimpleDelegator' do
|
196
|
+
proxy = Class.new(SimpleDelegator).new(users_proxy)
|
197
|
+
|
198
|
+
expect(users_proxy.select(:name)).to be_instance_of(ROM::Repository::RelationProxy)
|
199
|
+
expect(proxy.select(:name)).to be_instance_of(ROM::Repository::RelationProxy)
|
200
|
+
end
|
194
201
|
end
|
195
202
|
end
|
@@ -18,81 +18,111 @@ RSpec.describe 'struct builder', '#call' do
|
|
18
18
|
[:attribute, attr_double(:name, :String)]]]]
|
19
19
|
end
|
20
20
|
|
21
|
-
|
21
|
+
context 'ROM::Struct' do
|
22
|
+
before { builder[*input] }
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
it 'generates a struct for a given relation name and columns' do
|
25
|
+
struct = builder.class.cache[input.hash]
|
25
26
|
|
26
|
-
|
27
|
+
user = struct.new(id: 1, name: 'Jane')
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
expect(user.id).to be(1)
|
30
|
+
expect(user.name).to eql('Jane')
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
expect(user[:id]).to be(1)
|
33
|
+
expect(user[:name]).to eql('Jane')
|
33
34
|
|
34
|
-
|
35
|
+
expect(Hash[user]).to eql(id: 1, name: 'Jane')
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
expect(user.inspect).to eql('#<ROM::Struct::User id=1 name="Jane">')
|
38
|
+
expect(user.to_s).to match(/\A#<ROM::Struct::User:0x[0-9a-f]+>\z/)
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
it 'stores struct in the cache' do
|
42
|
+
expect(builder.class.cache[input.hash]).to be(builder[*input])
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
45
|
+
context 'with reserved keywords as attribute names' do
|
46
|
+
let(:input) do
|
47
|
+
[:users, [:header, [
|
48
|
+
[:attribute, attr_double(:id, :Int)],
|
49
|
+
[:attribute, attr_double(:name, :String)],
|
50
|
+
[:attribute, attr_double(:alias, :String)],
|
51
|
+
[:attribute, attr_double(:until, :Time)]]]]
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'allows to build a struct class without complaining' do
|
55
|
+
struct = builder.class.cache[input.hash]
|
56
|
+
|
57
|
+
user = struct.new(id: 1, name: 'Jane', alias: 'JD', until: Time.new(2030))
|
58
|
+
|
59
|
+
expect(user.id).to be(1)
|
60
|
+
expect(user.name).to eql('Jane')
|
61
|
+
expect(user.alias).to eql('JD')
|
62
|
+
expect(user.until).to eql(Time.new(2030))
|
63
|
+
end
|
51
64
|
end
|
52
65
|
|
53
|
-
it '
|
66
|
+
it 'raise a friendly error on missing keys' do
|
54
67
|
struct = builder.class.cache[input.hash]
|
55
68
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
expect(user.name).to eql('Jane')
|
60
|
-
expect(user.alias).to eql('JD')
|
61
|
-
expect(user.until).to eql(Time.new(2030))
|
69
|
+
expect { struct.new(id: 1) }.to raise_error(
|
70
|
+
Dry::Struct::Error, /:name is missing/
|
71
|
+
)
|
62
72
|
end
|
63
73
|
end
|
64
74
|
|
65
|
-
|
66
|
-
|
75
|
+
context 'custom entity container' do
|
76
|
+
before do
|
77
|
+
module Test
|
78
|
+
module Custom
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
67
82
|
|
68
|
-
|
69
|
-
|
70
|
-
)
|
71
|
-
end
|
83
|
+
let(:struct) { builder[*input] }
|
84
|
+
subject(:builder) { ROM::Repository::StructBuilder.new(Test::Custom) }
|
72
85
|
|
73
|
-
|
74
|
-
|
86
|
+
it 'generates a struct class inside a given module' do
|
87
|
+
expect(struct.name).to eql('Test::Custom::User')
|
88
|
+
user = struct.new(id: 1, name: 'Jane')
|
75
89
|
|
76
|
-
|
77
|
-
|
78
|
-
user = struct.new(id: 1, name: 'Jane')
|
90
|
+
expect(user.inspect).to eql(%q{#<Test::Custom::User id=1 name="Jane">})
|
91
|
+
end
|
79
92
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
93
|
+
it 'uses the existing class as a parent' do
|
94
|
+
class Test::Custom::User < ROM::Struct
|
95
|
+
def upcased_name
|
96
|
+
name.upcase
|
97
|
+
end
|
85
98
|
end
|
86
99
|
|
87
|
-
|
88
|
-
user = struct.new(id: 1, name: 'J' * 50)
|
100
|
+
user = struct.new(id: 1, name: 'Jane')
|
89
101
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
102
|
+
expect(user.upcased_name).to eql('JANE')
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'raises a nice error on missing attributes' do
|
106
|
+
class Test::Custom::User < ROM::Struct
|
107
|
+
def upcased_middle_name
|
108
|
+
middle_name.upcase
|
109
|
+
end
|
95
110
|
end
|
111
|
+
|
112
|
+
user = struct.new(id: 1, name: 'Jane')
|
113
|
+
|
114
|
+
expect {
|
115
|
+
user.upcased_middle_name
|
116
|
+
}.to raise_error(
|
117
|
+
ROM::Struct::MissingAttribute,
|
118
|
+
/not loaded attribute\?/
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'works with implicit coercions' do
|
123
|
+
user = struct.new(id: 1, name: 'Jane')
|
124
|
+
|
125
|
+
expect([user].flatten).to eql([user])
|
96
126
|
end
|
97
127
|
end
|
98
128
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rom-repository
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-05-
|
11
|
+
date: 2017-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rom
|
@@ -17,6 +17,9 @@ dependencies:
|
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.2.3
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -24,6 +27,9 @@ dependencies:
|
|
24
27
|
- - "~>"
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '3.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.2.3
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rom-mapper
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,20 +50,20 @@ dependencies:
|
|
44
50
|
requirements:
|
45
51
|
- - "~>"
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0.
|
53
|
+
version: '0.3'
|
48
54
|
- - ">="
|
49
55
|
- !ruby/object:Gem::Version
|
50
|
-
version: 0.
|
56
|
+
version: 0.3.1
|
51
57
|
type: :runtime
|
52
58
|
prerelease: false
|
53
59
|
version_requirements: !ruby/object:Gem::Requirement
|
54
60
|
requirements:
|
55
61
|
- - "~>"
|
56
62
|
- !ruby/object:Gem::Version
|
57
|
-
version: '0.
|
63
|
+
version: '0.3'
|
58
64
|
- - ">="
|
59
65
|
- !ruby/object:Gem::Version
|
60
|
-
version: 0.
|
66
|
+
version: 0.3.1
|
61
67
|
- !ruby/object:Gem::Dependency
|
62
68
|
name: dry-struct
|
63
69
|
requirement: !ruby/object:Gem::Requirement
|