rom 0.7.1 → 0.8.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -8
- data/CHANGELOG.md +28 -1
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +2 -2
- data/lib/rom.rb +1 -1
- data/lib/rom/command.rb +7 -5
- data/lib/rom/command_registry.rb +1 -1
- data/lib/rom/commands.rb +0 -2
- data/lib/rom/commands/abstract.rb +55 -25
- data/lib/rom/commands/composite.rb +13 -1
- data/lib/rom/commands/delete.rb +0 -8
- data/lib/rom/commands/graph.rb +102 -0
- data/lib/rom/commands/graph/class_interface.rb +69 -0
- data/lib/rom/commands/lazy.rb +87 -0
- data/lib/rom/constants.rb +22 -0
- data/lib/rom/env.rb +48 -18
- data/lib/rom/gateway.rb +132 -0
- data/lib/rom/global.rb +19 -19
- data/lib/rom/header.rb +42 -16
- data/lib/rom/header/attribute.rb +37 -15
- data/lib/rom/lint/gateway.rb +94 -0
- data/lib/rom/lint/spec.rb +15 -3
- data/lib/rom/lint/test.rb +45 -14
- data/lib/rom/mapper.rb +23 -10
- data/lib/rom/mapper/attribute_dsl.rb +157 -18
- data/lib/rom/memory.rb +1 -1
- data/lib/rom/memory/commands.rb +10 -8
- data/lib/rom/memory/dataset.rb +22 -2
- data/lib/rom/memory/{repository.rb → gateway.rb} +10 -10
- data/lib/rom/pipeline.rb +2 -1
- data/lib/rom/processor/transproc.rb +105 -14
- data/lib/rom/relation.rb +4 -4
- data/lib/rom/relation/class_interface.rb +19 -13
- data/lib/rom/relation/graph.rb +22 -0
- data/lib/rom/relation/lazy.rb +5 -3
- data/lib/rom/repository.rb +9 -118
- data/lib/rom/setup.rb +21 -14
- data/lib/rom/setup/finalize.rb +19 -19
- data/lib/rom/setup_dsl/relation.rb +10 -1
- data/lib/rom/support/deprecations.rb +21 -3
- data/lib/rom/support/enumerable_dataset.rb +1 -1
- data/lib/rom/version.rb +1 -1
- data/rom.gemspec +2 -4
- data/spec/integration/commands/delete_spec.rb +6 -0
- data/spec/integration/commands/graph_spec.rb +235 -0
- data/spec/integration/mappers/combine_spec.rb +14 -5
- data/spec/integration/mappers/definition_dsl_spec.rb +6 -1
- data/spec/integration/mappers/exclude_spec.rb +28 -0
- data/spec/integration/mappers/fold_spec.rb +16 -0
- data/spec/integration/mappers/group_spec.rb +0 -22
- data/spec/integration/mappers/prefix_separator_spec.rb +54 -0
- data/spec/integration/mappers/prefix_spec.rb +50 -0
- data/spec/integration/mappers/reusing_mappers_spec.rb +21 -0
- data/spec/integration/mappers/step_spec.rb +120 -0
- data/spec/integration/mappers/unfold_spec.rb +93 -0
- data/spec/integration/mappers/ungroup_spec.rb +127 -0
- data/spec/integration/mappers/unwrap_spec.rb +2 -2
- data/spec/integration/multi_repo_spec.rb +11 -11
- data/spec/integration/repositories/setting_logger_spec.rb +2 -2
- data/spec/integration/setup_spec.rb +11 -1
- data/spec/shared/command_behavior.rb +18 -0
- data/spec/shared/materializable.rb +4 -2
- data/spec/shared/users_and_tasks.rb +3 -3
- data/spec/test/memory_repository_lint_test.rb +4 -4
- data/spec/unit/rom/commands/graph_spec.rb +198 -0
- data/spec/unit/rom/commands/lazy_spec.rb +88 -0
- data/spec/unit/rom/commands_spec.rb +2 -2
- data/spec/unit/rom/env_spec.rb +26 -0
- data/spec/unit/rom/gateway_spec.rb +90 -0
- data/spec/unit/rom/global_spec.rb +4 -3
- data/spec/unit/rom/mapper/dsl_spec.rb +42 -1
- data/spec/unit/rom/mapper_spec.rb +4 -1
- data/spec/unit/rom/memory/commands/create_spec.rb +21 -0
- data/spec/unit/rom/memory/commands/delete_spec.rb +21 -0
- data/spec/unit/rom/memory/commands/update_spec.rb +21 -0
- data/spec/unit/rom/memory/relation_spec.rb +42 -10
- data/spec/unit/rom/memory/repository_spec.rb +3 -3
- data/spec/unit/rom/processor/transproc_spec.rb +75 -0
- data/spec/unit/rom/relation/lazy/combine_spec.rb +33 -4
- data/spec/unit/rom/relation/lazy_spec.rb +9 -1
- data/spec/unit/rom/repository_spec.rb +4 -63
- data/spec/unit/rom/setup_spec.rb +19 -5
- metadata +28 -38
- data/.ruby-version +0 -1
- data/lib/rom/lint/repository.rb +0 -94
@@ -0,0 +1,69 @@
|
|
1
|
+
module ROM
|
2
|
+
module Commands
|
3
|
+
class Graph
|
4
|
+
# Class methods for command Graph
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module ClassInterface
|
8
|
+
# Build a command graph recursively
|
9
|
+
#
|
10
|
+
# This is used by `Env#command` when array with options is passed in
|
11
|
+
#
|
12
|
+
# @param [Registry] registry The command registry from env
|
13
|
+
# @param [Array] options The options array
|
14
|
+
# @param [Array] path The path for input evaluator proc
|
15
|
+
#
|
16
|
+
# @return [Graph]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def build(registry, options, path = EMPTY_ARRAY)
|
20
|
+
options.reduce { |spec, other| build_command(registry, spec, other, path) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def build_command(registry, spec, other, path)
|
25
|
+
name, nodes = other
|
26
|
+
|
27
|
+
key, relation =
|
28
|
+
if spec.is_a?(Hash)
|
29
|
+
spec.to_a.first
|
30
|
+
else
|
31
|
+
[spec, spec]
|
32
|
+
end
|
33
|
+
|
34
|
+
command = registry[relation][name]
|
35
|
+
|
36
|
+
tuple_path = Array[*path] << key
|
37
|
+
|
38
|
+
input_proc = -> *args do
|
39
|
+
input, index = args
|
40
|
+
|
41
|
+
begin
|
42
|
+
if index
|
43
|
+
tuple_path[0..tuple_path.size-2]
|
44
|
+
.reduce(input) { |a,e| a.fetch(e) }
|
45
|
+
.at(index)[tuple_path.last]
|
46
|
+
else
|
47
|
+
tuple_path.reduce(input) { |a,e| a.fetch(e) }
|
48
|
+
end
|
49
|
+
rescue KeyError => err
|
50
|
+
raise CommandFailure.new(command, err)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
command = command.with(input_proc)
|
55
|
+
|
56
|
+
if nodes
|
57
|
+
if nodes.all? { |node| node.is_a?(Array) }
|
58
|
+
command.combine(*nodes.map { |node| build(registry, node, tuple_path) })
|
59
|
+
else
|
60
|
+
command.combine(build(registry, nodes, tuple_path))
|
61
|
+
end
|
62
|
+
else
|
63
|
+
command
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rom/commands/composite'
|
2
|
+
require 'rom/commands/graph'
|
3
|
+
|
4
|
+
module ROM
|
5
|
+
module Commands
|
6
|
+
# Lazy command wraps another command and evaluates its input when called
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Lazy
|
10
|
+
# @attr_reader [Command] command The wrapped command
|
11
|
+
attr_reader :command
|
12
|
+
|
13
|
+
# @attr_reader [Proc] evaluator The proc that will evaluate the input
|
14
|
+
attr_reader :evaluator
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
def initialize(command, evaluator)
|
18
|
+
@command = command
|
19
|
+
@evaluator = evaluator
|
20
|
+
end
|
21
|
+
|
22
|
+
# Evaluate command's input using the input proc and pass to command
|
23
|
+
#
|
24
|
+
# @return [Array,Hash]
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def call(*args)
|
28
|
+
first = args.first
|
29
|
+
last = args.last
|
30
|
+
size = args.size
|
31
|
+
|
32
|
+
if size > 1 && last.is_a?(Array)
|
33
|
+
last.map.with_index do |item, index|
|
34
|
+
command.call(evaluator[first, index], item)
|
35
|
+
end.reduce(:concat)
|
36
|
+
else
|
37
|
+
command.call(evaluator[first], *args[1..size-1])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Compose a lazy command with another one
|
42
|
+
#
|
43
|
+
# @see Commands::Abstract#>>
|
44
|
+
#
|
45
|
+
# @return [Composite]
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def >>(other)
|
49
|
+
Composite.new(self, other)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @see Abstract#combine
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def combine(*others)
|
56
|
+
Graph.new(self, others)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
def lazy?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def respond_to_missing?(name, include_private = false)
|
66
|
+
super || command.respond_to?(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# @api private
|
72
|
+
def method_missing(name, *args, &block)
|
73
|
+
if command.respond_to?(name)
|
74
|
+
response = command.public_send(name, *args, &block)
|
75
|
+
|
76
|
+
if response.instance_of?(command.class)
|
77
|
+
self.class.new(response, evaluator)
|
78
|
+
else
|
79
|
+
response
|
80
|
+
end
|
81
|
+
else
|
82
|
+
super
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/rom/constants.rb
CHANGED
@@ -4,17 +4,39 @@ module ROM
|
|
4
4
|
|
5
5
|
AdapterLoadError = Class.new(StandardError)
|
6
6
|
|
7
|
+
class AdapterNotPresentError < StandardError
|
8
|
+
def initialize(adapter, component)
|
9
|
+
super(
|
10
|
+
"Failed to find #{component} class for #{adapter} adapter. " \
|
11
|
+
"Make sure ROM setup was started and the adapter identifier is correct."
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
7
16
|
EnvAlreadyFinalizedError = Class.new(StandardError)
|
8
17
|
RelationAlreadyDefinedError = Class.new(StandardError)
|
9
18
|
NoRelationError = Class.new(StandardError)
|
10
19
|
CommandError = Class.new(StandardError)
|
11
20
|
TupleCountMismatchError = Class.new(CommandError)
|
12
21
|
MapperMissingError = Class.new(StandardError)
|
22
|
+
MapperMisconfiguredError = Class.new(StandardError)
|
13
23
|
UnknownPluginError = Class.new(StandardError)
|
24
|
+
UnsupportedRelationError = Class.new(StandardError)
|
14
25
|
|
15
26
|
InvalidOptionValueError = Class.new(StandardError)
|
16
27
|
InvalidOptionKeyError = Class.new(StandardError)
|
17
28
|
|
29
|
+
class CommandFailure < StandardError
|
30
|
+
attr_reader :command
|
31
|
+
attr_reader :original_error
|
32
|
+
|
33
|
+
def initialize(command, err)
|
34
|
+
super("command: #{command.inspect}; original message: #{err.message}")
|
35
|
+
@command = command
|
36
|
+
@original_error = original_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
18
40
|
EMPTY_ARRAY = [].freeze
|
19
41
|
EMPTY_HASH = {}.freeze
|
20
42
|
end
|
data/lib/rom/env.rb
CHANGED
@@ -1,27 +1,30 @@
|
|
1
1
|
require 'rom/relation/loaded'
|
2
|
+
require 'rom/commands/graph'
|
2
3
|
require 'rom/support/deprecations'
|
3
4
|
|
4
5
|
module ROM
|
5
|
-
# Exposes defined
|
6
|
+
# Exposes defined gateways, relations and mappers
|
6
7
|
#
|
7
8
|
# @api public
|
8
9
|
class Env
|
9
10
|
extend Deprecations
|
10
|
-
include Equalizer.new(:
|
11
|
+
include Equalizer.new(:gateways, :relations, :mappers, :commands)
|
11
12
|
|
12
|
-
# @return [Hash] configured
|
13
|
+
# @return [Hash] configured gateways
|
13
14
|
#
|
14
15
|
# @api public
|
15
|
-
attr_reader :
|
16
|
+
attr_reader :gateways
|
17
|
+
|
18
|
+
deprecate :repositories, :gateways
|
16
19
|
|
17
20
|
# @return [RelationRegistry] relation registry
|
18
21
|
#
|
19
|
-
# @api
|
22
|
+
# @api private
|
20
23
|
attr_reader :relations
|
21
24
|
|
22
25
|
# @return [Registry] command registry
|
23
26
|
#
|
24
|
-
# @api
|
27
|
+
# @api private
|
25
28
|
attr_reader :commands
|
26
29
|
|
27
30
|
# @return [Registry] mapper registry
|
@@ -30,31 +33,34 @@ module ROM
|
|
30
33
|
attr_reader :mappers
|
31
34
|
|
32
35
|
# @api private
|
33
|
-
def initialize(
|
34
|
-
@
|
36
|
+
def initialize(gateways, relations, mappers, commands)
|
37
|
+
@gateways = gateways
|
35
38
|
@relations = relations
|
36
39
|
@mappers = mappers
|
37
40
|
@commands = commands
|
38
41
|
freeze
|
39
42
|
end
|
40
43
|
|
41
|
-
#
|
44
|
+
# Get lazy relation identified by its name
|
42
45
|
#
|
43
46
|
# @example
|
44
|
-
#
|
45
47
|
# rom.relation(:users)
|
46
|
-
# rom.relation(:users)
|
48
|
+
# rom.relation(:users).by_name('Jane')
|
49
|
+
#
|
50
|
+
# # block syntax allows accessing lower-level query DSLs (usage is discouraged though)
|
51
|
+
# rom.relation { |r| r.restrict(name: 'Jane') }
|
47
52
|
#
|
48
53
|
# # with mapping
|
49
54
|
# rom.relation(:users).map_with(:presenter)
|
50
55
|
#
|
51
|
-
#
|
56
|
+
# # using multiple mappers
|
57
|
+
# rom.relation(:users).page(1).map_with(:presenter, :json_serializer)
|
52
58
|
#
|
53
59
|
# @param [Symbol] name of the relation to load
|
54
60
|
#
|
55
61
|
# @yield [Relation]
|
56
62
|
#
|
57
|
-
# @return [Relation::
|
63
|
+
# @return [Relation::Lazy]
|
58
64
|
#
|
59
65
|
# @api public
|
60
66
|
def relation(name, &block)
|
@@ -80,15 +86,39 @@ module ROM
|
|
80
86
|
# # plain command returning tuples
|
81
87
|
# rom.command(:users).create
|
82
88
|
#
|
83
|
-
# #
|
89
|
+
# # allows auto-mapping using registered mappers
|
84
90
|
# rom.command(:users).as(:entity)
|
85
91
|
#
|
92
|
+
# # allows building up a command graph for nested input
|
93
|
+
# command = rom.command([:users, [:create, [:tasks, [:create]]]])
|
94
|
+
#
|
95
|
+
# command.call(users: [{ name: 'Jane', tasks: [{ title: 'One' }] }])
|
96
|
+
#
|
97
|
+
# @param [Array,Symbol] options Either graph options or registered command name
|
98
|
+
#
|
99
|
+
# @return [Command, Command::Graph]
|
100
|
+
#
|
86
101
|
# @api public
|
87
|
-
def command(
|
88
|
-
|
89
|
-
|
102
|
+
def command(options)
|
103
|
+
case options
|
104
|
+
when Symbol
|
105
|
+
name = options
|
106
|
+
if mappers.key?(name)
|
107
|
+
commands[name].with(mappers: mappers[name])
|
108
|
+
else
|
109
|
+
commands[name]
|
110
|
+
end
|
111
|
+
when Array
|
112
|
+
graph = Commands::Graph.build(commands, options)
|
113
|
+
name = graph.name
|
114
|
+
|
115
|
+
if mappers.key?(name)
|
116
|
+
graph.with(mappers: mappers[graph.name])
|
117
|
+
else
|
118
|
+
graph
|
119
|
+
end
|
90
120
|
else
|
91
|
-
|
121
|
+
raise ArgumentError, "#{self.class}#command accepts a symbol or an array"
|
92
122
|
end
|
93
123
|
end
|
94
124
|
end
|
data/lib/rom/gateway.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
module ROM
|
2
|
+
# Abstract gateway class
|
3
|
+
#
|
4
|
+
# @api public
|
5
|
+
class Gateway
|
6
|
+
# Return connection object
|
7
|
+
#
|
8
|
+
# @return [Object] type varies depending on the gateway
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
attr_reader :connection
|
12
|
+
|
13
|
+
# Setup a gateway
|
14
|
+
#
|
15
|
+
# @overload setup(type, *args)
|
16
|
+
# Sets up a single-gateway given a gateway type.
|
17
|
+
# For custom gateways, create an instance and pass it directly.
|
18
|
+
#
|
19
|
+
# @param [Symbol] type
|
20
|
+
# @param [Array] *args
|
21
|
+
#
|
22
|
+
# @overload setup(gateway)
|
23
|
+
# @param [Gateway] gateway
|
24
|
+
#
|
25
|
+
# @return [Gateway] a specific gateway subclass
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# module SuperDB
|
29
|
+
# class Gateway < ROM::Gateway
|
30
|
+
# def initialize(options)
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# ROM.register_adapter(:super_db, SuperDB)
|
36
|
+
#
|
37
|
+
# Gateway.setup(:super_db, some: 'options')
|
38
|
+
# # SuperDB::Gateway.new(some: 'options') is called
|
39
|
+
#
|
40
|
+
# # or alternatively
|
41
|
+
# super_db = Gateway.setup(SuperDB::Gateway.new(some: 'options'))
|
42
|
+
# Gateway.setup(super_db)
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def self.setup(gateway_or_scheme, *args)
|
46
|
+
case gateway_or_scheme
|
47
|
+
when String
|
48
|
+
raise ArgumentError, <<-STRING.gsub(/^ {10}/, '')
|
49
|
+
URIs without an explicit scheme are not supported anymore.
|
50
|
+
See https://github.com/rom-rb/rom/blob/master/CHANGELOG.md
|
51
|
+
STRING
|
52
|
+
when Symbol
|
53
|
+
class_from_symbol(gateway_or_scheme).new(*args)
|
54
|
+
else
|
55
|
+
if args.empty?
|
56
|
+
gateway_or_scheme
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Can't accept arguments when passing an instance"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get gateway subclass for a specific adapter
|
64
|
+
#
|
65
|
+
# @param [Symbol] type adapter identifier
|
66
|
+
#
|
67
|
+
# @return [Class]
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def self.class_from_symbol(type)
|
71
|
+
adapter = ROM.adapters.fetch(type) do
|
72
|
+
begin
|
73
|
+
require "rom/#{type}"
|
74
|
+
rescue LoadError
|
75
|
+
raise AdapterLoadError, "Failed to load adapter rom/#{type}"
|
76
|
+
end
|
77
|
+
|
78
|
+
ROM.adapters.fetch(type)
|
79
|
+
end
|
80
|
+
|
81
|
+
if adapter.const_defined?(:Gateway)
|
82
|
+
adapter.const_get(:Gateway)
|
83
|
+
else
|
84
|
+
adapter.const_get(:Repository)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# A generic interface for setting up a logger
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def use_logger(*)
|
92
|
+
# noop
|
93
|
+
end
|
94
|
+
|
95
|
+
# A generic interface for returning default logger
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
def logger
|
99
|
+
# noop
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extension hook for adding gateway-specific behavior to a command class
|
103
|
+
#
|
104
|
+
# @param [Class] klass command class
|
105
|
+
# @param [Object] _dataset dataset that will be used with this command class
|
106
|
+
#
|
107
|
+
# @return [Class]
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def extend_command_class(klass, _dataset)
|
111
|
+
klass
|
112
|
+
end
|
113
|
+
|
114
|
+
# Schema inference hook
|
115
|
+
#
|
116
|
+
# Every gateway that supports schema inference should implement this method
|
117
|
+
#
|
118
|
+
# @return [Array] array with datasets and their names
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
def schema
|
122
|
+
[]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Disconnect is optional and it's a no-op by default
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def disconnect
|
129
|
+
# noop
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|