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