rom 0.3.1 → 0.4.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/CHANGELOG.md +20 -0
- data/Gemfile +2 -0
- data/README.md +20 -5
- data/lib/rom.rb +12 -2
- data/lib/rom/adapter.rb +110 -4
- data/lib/rom/adapter/memory.rb +9 -52
- data/lib/rom/adapter/memory/commands.rb +41 -0
- data/lib/rom/adapter/memory/dataset.rb +52 -0
- data/lib/rom/adapter/memory/storage.rb +25 -0
- data/lib/rom/boot.rb +28 -1
- data/lib/rom/boot/command_dsl.rb +48 -0
- data/lib/rom/boot/dsl.rb +7 -0
- data/lib/rom/command_registry.rb +68 -0
- data/lib/rom/commands.rb +64 -0
- data/lib/rom/commands/create.rb +28 -0
- data/lib/rom/commands/delete.rb +44 -0
- data/lib/rom/commands/update.rb +42 -0
- data/lib/rom/commands/with_options.rb +21 -0
- data/lib/rom/env.rb +22 -6
- data/lib/rom/relation.rb +0 -18
- data/lib/rom/repository.rb +25 -3
- data/lib/rom/version.rb +1 -1
- data/spec/integration/adapters/setting_logger_spec.rb +35 -0
- data/spec/integration/commands/create_spec.rb +96 -0
- data/spec/integration/commands/delete_spec.rb +70 -0
- data/spec/integration/commands/error_handling_spec.rb +23 -0
- data/spec/integration/commands/try_spec.rb +27 -0
- data/spec/integration/commands/update_spec.rb +106 -0
- data/spec/integration/setup_spec.rb +35 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/mutant.rb +7 -0
- data/spec/unit/rom/env_spec.rb +19 -0
- data/spec/unit/rom/repository_spec.rb +21 -0
- metadata +22 -2
@@ -0,0 +1,25 @@
|
|
1
|
+
module ROM
|
2
|
+
class Adapter
|
3
|
+
class Memory < Adapter
|
4
|
+
|
5
|
+
class Storage
|
6
|
+
attr_reader :data
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
@data = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](name)
|
14
|
+
data[name] ||= Dataset.new([])
|
15
|
+
end
|
16
|
+
|
17
|
+
def key?(name)
|
18
|
+
data.key?(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/rom/boot.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rom/boot/dsl'
|
2
2
|
require 'rom/relation_builder'
|
3
3
|
require 'rom/reader_builder'
|
4
|
+
require 'rom/command_registry'
|
4
5
|
|
5
6
|
module ROM
|
6
7
|
|
@@ -18,6 +19,7 @@ module ROM
|
|
18
19
|
@schema = {}
|
19
20
|
@relations = {}
|
20
21
|
@mappers = []
|
22
|
+
@commands = {}
|
21
23
|
@adapter_relation_map = {}
|
22
24
|
@env = nil
|
23
25
|
end
|
@@ -77,6 +79,10 @@ module ROM
|
|
77
79
|
self
|
78
80
|
end
|
79
81
|
|
82
|
+
def commands(name, &block)
|
83
|
+
@commands.update(name => DSL.new(self).commands(&block))
|
84
|
+
end
|
85
|
+
|
80
86
|
# Finalize the setup
|
81
87
|
#
|
82
88
|
# @return [Env] frozen env with access to repositories, schema, relations and mappers
|
@@ -88,8 +94,9 @@ module ROM
|
|
88
94
|
schema = load_schema
|
89
95
|
relations = load_relations(schema)
|
90
96
|
readers = load_readers(relations)
|
97
|
+
commands = load_commands(relations)
|
91
98
|
|
92
|
-
@env = Env.new(repositories, schema, relations, readers)
|
99
|
+
@env = Env.new(repositories, schema, relations, readers, commands)
|
93
100
|
end
|
94
101
|
|
95
102
|
# @api private
|
@@ -162,6 +169,26 @@ module ROM
|
|
162
169
|
ReaderRegistry.new(readers)
|
163
170
|
end
|
164
171
|
|
172
|
+
def load_commands(relations)
|
173
|
+
return Registry.new unless relations.elements.any?
|
174
|
+
|
175
|
+
commands = @commands.each_with_object({}) do |(name, definitions), h|
|
176
|
+
adapter = adapter_relation_map[name]
|
177
|
+
|
178
|
+
rel_commands = {}
|
179
|
+
|
180
|
+
definitions.each do |command_name, definition|
|
181
|
+
rel_commands[command_name] = adapter.command(
|
182
|
+
command_name, relations[name], definition
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
h[name] = CommandRegistry.new(rel_commands)
|
187
|
+
end
|
188
|
+
|
189
|
+
Registry.new(commands)
|
190
|
+
end
|
191
|
+
|
165
192
|
end
|
166
193
|
|
167
194
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rom/mapper_builder'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
|
5
|
+
class CommandDSL
|
6
|
+
attr_reader :commands
|
7
|
+
|
8
|
+
class CommandDefinition
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
def initialize(options, &block)
|
12
|
+
@options = options
|
13
|
+
instance_exec(&block) if block
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
options
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
options[:type]
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(name, *args, &block)
|
25
|
+
if args.size == 1
|
26
|
+
options[name] = args.first
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@commands = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def call
|
38
|
+
commands
|
39
|
+
end
|
40
|
+
|
41
|
+
def define(name, options = {}, &block)
|
42
|
+
commands[name] = CommandDefinition.new(options, &block)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/rom/boot/dsl.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rom/boot/schema_dsl'
|
2
2
|
require 'rom/boot/mapper_dsl'
|
3
|
+
require 'rom/boot/command_dsl'
|
3
4
|
|
4
5
|
module ROM
|
5
6
|
class Boot
|
@@ -24,6 +25,12 @@ module ROM
|
|
24
25
|
dsl.call
|
25
26
|
end
|
26
27
|
|
28
|
+
def commands(&block)
|
29
|
+
dsl = CommandDSL.new
|
30
|
+
dsl.instance_exec(&block)
|
31
|
+
dsl.call
|
32
|
+
end
|
33
|
+
|
27
34
|
end
|
28
35
|
|
29
36
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ROM
|
2
|
+
|
3
|
+
class Result
|
4
|
+
attr_reader :value, :error
|
5
|
+
|
6
|
+
def to_ary
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
alias_method :to_a, :to_ary
|
10
|
+
|
11
|
+
class Success < Result
|
12
|
+
def initialize(value)
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def >(f)
|
17
|
+
f.call(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_ary
|
21
|
+
value
|
22
|
+
end
|
23
|
+
alias_method :to_a, :to_ary
|
24
|
+
end
|
25
|
+
|
26
|
+
class Failure < Result
|
27
|
+
def initialize(error)
|
28
|
+
@error = error
|
29
|
+
end
|
30
|
+
|
31
|
+
def >(f)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_ary
|
36
|
+
error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class CommandRegistry < Registry
|
42
|
+
|
43
|
+
class Evaluator
|
44
|
+
include Concord.new(:registry)
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def method_missing(name, *args, &block)
|
49
|
+
command = registry[name]
|
50
|
+
|
51
|
+
super unless command
|
52
|
+
|
53
|
+
if args.size > 1
|
54
|
+
command.new(*args, &block)
|
55
|
+
else
|
56
|
+
command.call(*args, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def try(&f)
|
62
|
+
Result::Success.new(Evaluator.new(self).instance_exec(&f))
|
63
|
+
rescue CommandError => e
|
64
|
+
Result::Failure.new(e)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/lib/rom/commands.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module ROM
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
class AbstractCommand
|
5
|
+
VALID_RESULTS = [:one, :many].freeze
|
6
|
+
|
7
|
+
attr_reader :relation, :options, :result
|
8
|
+
|
9
|
+
# @api private
|
10
|
+
def initialize(relation, options)
|
11
|
+
@relation = relation
|
12
|
+
@options = options
|
13
|
+
|
14
|
+
@result = options[:result] || :many
|
15
|
+
|
16
|
+
if !VALID_RESULTS.include?(result)
|
17
|
+
raise InvalidOptionError.new(:result, VALID_RESULTS)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Call the command and return one or many tuples
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def call(*args)
|
25
|
+
tuples = execute(*args)
|
26
|
+
|
27
|
+
if result == :one
|
28
|
+
tuples.first
|
29
|
+
else
|
30
|
+
tuples
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Target relation on which the command will operate
|
35
|
+
#
|
36
|
+
# By default this is set to the relation that's passed to the constructor.
|
37
|
+
# Specialized commands like Delete may set the target to a different relation.
|
38
|
+
#
|
39
|
+
# @return [Relation]
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def target
|
43
|
+
relation
|
44
|
+
end
|
45
|
+
|
46
|
+
# Assert that tuple count in the target relation corresponds to :result setting
|
47
|
+
#
|
48
|
+
# @raises TupleCountMismatchError
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
def assert_tuple_count
|
52
|
+
if result == :one && target.size > 1
|
53
|
+
raise TupleCountMismatchError, "#{inspect} expects one tuple"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
require 'rom/commands/create'
|
63
|
+
require 'rom/commands/update'
|
64
|
+
require 'rom/commands/delete'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rom/commands/with_options'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Commands
|
5
|
+
|
6
|
+
# Create command
|
7
|
+
#
|
8
|
+
# This command inserts a new tuple into a relation
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
class Create < AbstractCommand
|
12
|
+
include WithOptions
|
13
|
+
|
14
|
+
# Execute the command
|
15
|
+
#
|
16
|
+
# @abstract
|
17
|
+
#
|
18
|
+
# @return [Array] an array with inserted tuples
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
def execute(tuple)
|
22
|
+
raise NotImplementedError, "#{self.class}##{__method__} must be implemented"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ROM
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Delete command
|
5
|
+
#
|
6
|
+
# This command removes tuples from its target relation
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
class Delete < AbstractCommand
|
10
|
+
attr_reader :target
|
11
|
+
|
12
|
+
def initialize(relation, options)
|
13
|
+
super
|
14
|
+
@target = options[:target] || relation
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see AbstractCommand#call
|
18
|
+
def call(*args)
|
19
|
+
assert_tuple_count
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
# Execute the command
|
24
|
+
#
|
25
|
+
# @abstract
|
26
|
+
#
|
27
|
+
# @return [Array] an array with removed tuples
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def execute
|
31
|
+
raise NotImplementedError, "#{self.class}##{__method__} must be implemented"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return new delete command with new target
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def new(*args, &block)
|
38
|
+
self.class.new(relation, options.merge(target: relation.public_send(*args, &block)))
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rom/commands/with_options'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Commands
|
5
|
+
|
6
|
+
# Update command
|
7
|
+
#
|
8
|
+
# This command updates all tuples in its relation with new attributes
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
class Update < AbstractCommand
|
12
|
+
include WithOptions
|
13
|
+
|
14
|
+
alias_method :set, :call
|
15
|
+
|
16
|
+
# @see AbstractCommand#call
|
17
|
+
def call(*args)
|
18
|
+
assert_tuple_count
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
# Execute the update command
|
23
|
+
#
|
24
|
+
# @return [Array] an array with updated tuples
|
25
|
+
#
|
26
|
+
# @abstract
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
def execute(params)
|
30
|
+
raise NotImplementedError, "#{self.class}##{__method__} must be implemented"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return new update command with new relation
|
34
|
+
#
|
35
|
+
# @api private
|
36
|
+
def new(*args, &block)
|
37
|
+
self.class.new(relation.public_send(*args, &block), options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ROM
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Common behavior for Create and Update commands
|
5
|
+
#
|
6
|
+
# TODO: find a better name for this module
|
7
|
+
module WithOptions
|
8
|
+
attr_reader :validator, :input
|
9
|
+
|
10
|
+
# @api private
|
11
|
+
def initialize(relation, options)
|
12
|
+
super
|
13
|
+
|
14
|
+
@validator = options[:validator] || Proc.new {}
|
15
|
+
@input = options[:input] || Hash
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/rom/env.rb
CHANGED
@@ -5,16 +5,17 @@ module ROM
|
|
5
5
|
# @api public
|
6
6
|
class Env
|
7
7
|
include Adamantium::Flat
|
8
|
-
include Equalizer.new(:repositories, :schema, :relations, :mappers)
|
8
|
+
include Equalizer.new(:repositories, :schema, :relations, :mappers, :commands)
|
9
9
|
|
10
|
-
attr_reader :repositories, :schema, :relations, :mappers
|
10
|
+
attr_reader :repositories, :schema, :relations, :mappers, :commands
|
11
11
|
|
12
12
|
# @api private
|
13
|
-
def initialize(repositories, schema, relations, mappers)
|
13
|
+
def initialize(repositories, schema, relations, mappers, commands)
|
14
14
|
@repositories = repositories
|
15
15
|
@schema = schema
|
16
16
|
@relations = relations
|
17
17
|
@mappers = mappers
|
18
|
+
@commands = commands
|
18
19
|
end
|
19
20
|
|
20
21
|
# Returns a reader with access to defined mappers
|
@@ -28,6 +29,17 @@ module ROM
|
|
28
29
|
mappers[name]
|
29
30
|
end
|
30
31
|
|
32
|
+
# Returns commands registry for the given relation
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
#
|
36
|
+
# rom.command(:users).create
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def command(name)
|
40
|
+
commands[name]
|
41
|
+
end
|
42
|
+
|
31
43
|
# @api private
|
32
44
|
def [](name)
|
33
45
|
repositories.fetch(name)
|
@@ -35,14 +47,18 @@ module ROM
|
|
35
47
|
|
36
48
|
# @api private
|
37
49
|
def respond_to_missing?(name, include_private = false)
|
38
|
-
repositories.key?(name)
|
50
|
+
repositories.key?(name) || super
|
39
51
|
end
|
40
52
|
|
41
53
|
private
|
42
54
|
|
43
55
|
# @api private
|
44
|
-
def method_missing(name, *args)
|
45
|
-
repositories.
|
56
|
+
def method_missing(name, *args, &block)
|
57
|
+
if repositories.key?(name)
|
58
|
+
repositories[name]
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
46
62
|
end
|
47
63
|
end
|
48
64
|
|