rom-repository 1.3.2 → 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 Version](https://badge.fury.io/rb/rom-repository.svg)][gem]
|
10
|
-
[![Build Status](https://travis-ci.org/rom-rb/rom-repository.svg?branch=master)][travis]
|
11
|
-
[![Dependency Status](https://gemnasium.com/rom-rb/rom-repository.svg)][gemnasium]
|
12
|
-
[![Code Climate](https://codeclimate.com/github/rom-rb/rom-repository/badges/gpa.svg)][codeclimate]
|
13
|
-
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
|
14
|
-
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][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
|