rom 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|