rom 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +5 -8
  4. data/CHANGELOG.md +28 -1
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +2 -2
  7. data/lib/rom.rb +1 -1
  8. data/lib/rom/command.rb +7 -5
  9. data/lib/rom/command_registry.rb +1 -1
  10. data/lib/rom/commands.rb +0 -2
  11. data/lib/rom/commands/abstract.rb +55 -25
  12. data/lib/rom/commands/composite.rb +13 -1
  13. data/lib/rom/commands/delete.rb +0 -8
  14. data/lib/rom/commands/graph.rb +102 -0
  15. data/lib/rom/commands/graph/class_interface.rb +69 -0
  16. data/lib/rom/commands/lazy.rb +87 -0
  17. data/lib/rom/constants.rb +22 -0
  18. data/lib/rom/env.rb +48 -18
  19. data/lib/rom/gateway.rb +132 -0
  20. data/lib/rom/global.rb +19 -19
  21. data/lib/rom/header.rb +42 -16
  22. data/lib/rom/header/attribute.rb +37 -15
  23. data/lib/rom/lint/gateway.rb +94 -0
  24. data/lib/rom/lint/spec.rb +15 -3
  25. data/lib/rom/lint/test.rb +45 -14
  26. data/lib/rom/mapper.rb +23 -10
  27. data/lib/rom/mapper/attribute_dsl.rb +157 -18
  28. data/lib/rom/memory.rb +1 -1
  29. data/lib/rom/memory/commands.rb +10 -8
  30. data/lib/rom/memory/dataset.rb +22 -2
  31. data/lib/rom/memory/{repository.rb → gateway.rb} +10 -10
  32. data/lib/rom/pipeline.rb +2 -1
  33. data/lib/rom/processor/transproc.rb +105 -14
  34. data/lib/rom/relation.rb +4 -4
  35. data/lib/rom/relation/class_interface.rb +19 -13
  36. data/lib/rom/relation/graph.rb +22 -0
  37. data/lib/rom/relation/lazy.rb +5 -3
  38. data/lib/rom/repository.rb +9 -118
  39. data/lib/rom/setup.rb +21 -14
  40. data/lib/rom/setup/finalize.rb +19 -19
  41. data/lib/rom/setup_dsl/relation.rb +10 -1
  42. data/lib/rom/support/deprecations.rb +21 -3
  43. data/lib/rom/support/enumerable_dataset.rb +1 -1
  44. data/lib/rom/version.rb +1 -1
  45. data/rom.gemspec +2 -4
  46. data/spec/integration/commands/delete_spec.rb +6 -0
  47. data/spec/integration/commands/graph_spec.rb +235 -0
  48. data/spec/integration/mappers/combine_spec.rb +14 -5
  49. data/spec/integration/mappers/definition_dsl_spec.rb +6 -1
  50. data/spec/integration/mappers/exclude_spec.rb +28 -0
  51. data/spec/integration/mappers/fold_spec.rb +16 -0
  52. data/spec/integration/mappers/group_spec.rb +0 -22
  53. data/spec/integration/mappers/prefix_separator_spec.rb +54 -0
  54. data/spec/integration/mappers/prefix_spec.rb +50 -0
  55. data/spec/integration/mappers/reusing_mappers_spec.rb +21 -0
  56. data/spec/integration/mappers/step_spec.rb +120 -0
  57. data/spec/integration/mappers/unfold_spec.rb +93 -0
  58. data/spec/integration/mappers/ungroup_spec.rb +127 -0
  59. data/spec/integration/mappers/unwrap_spec.rb +2 -2
  60. data/spec/integration/multi_repo_spec.rb +11 -11
  61. data/spec/integration/repositories/setting_logger_spec.rb +2 -2
  62. data/spec/integration/setup_spec.rb +11 -1
  63. data/spec/shared/command_behavior.rb +18 -0
  64. data/spec/shared/materializable.rb +4 -2
  65. data/spec/shared/users_and_tasks.rb +3 -3
  66. data/spec/test/memory_repository_lint_test.rb +4 -4
  67. data/spec/unit/rom/commands/graph_spec.rb +198 -0
  68. data/spec/unit/rom/commands/lazy_spec.rb +88 -0
  69. data/spec/unit/rom/commands_spec.rb +2 -2
  70. data/spec/unit/rom/env_spec.rb +26 -0
  71. data/spec/unit/rom/gateway_spec.rb +90 -0
  72. data/spec/unit/rom/global_spec.rb +4 -3
  73. data/spec/unit/rom/mapper/dsl_spec.rb +42 -1
  74. data/spec/unit/rom/mapper_spec.rb +4 -1
  75. data/spec/unit/rom/memory/commands/create_spec.rb +21 -0
  76. data/spec/unit/rom/memory/commands/delete_spec.rb +21 -0
  77. data/spec/unit/rom/memory/commands/update_spec.rb +21 -0
  78. data/spec/unit/rom/memory/relation_spec.rb +42 -10
  79. data/spec/unit/rom/memory/repository_spec.rb +3 -3
  80. data/spec/unit/rom/processor/transproc_spec.rb +75 -0
  81. data/spec/unit/rom/relation/lazy/combine_spec.rb +33 -4
  82. data/spec/unit/rom/relation/lazy_spec.rb +9 -1
  83. data/spec/unit/rom/repository_spec.rb +4 -63
  84. data/spec/unit/rom/setup_spec.rb +19 -5
  85. metadata +28 -38
  86. data/.ruby-version +0 -1
  87. 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 repositories, relations and mappers
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(:repositories, :relations, :mappers, :commands)
11
+ include Equalizer.new(:gateways, :relations, :mappers, :commands)
11
12
 
12
- # @return [Hash] configured repositories
13
+ # @return [Hash] configured gateways
13
14
  #
14
15
  # @api public
15
- attr_reader :repositories
16
+ attr_reader :gateways
17
+
18
+ deprecate :repositories, :gateways
16
19
 
17
20
  # @return [RelationRegistry] relation registry
18
21
  #
19
- # @api public
22
+ # @api private
20
23
  attr_reader :relations
21
24
 
22
25
  # @return [Registry] command registry
23
26
  #
24
- # @api public
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(repositories, relations, mappers, commands)
34
- @repositories = repositories
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
- # Load relation by name
44
+ # Get lazy relation identified by its name
42
45
  #
43
46
  # @example
44
- #
45
47
  # rom.relation(:users)
46
- # rom.relation(:users) { |r| r.by_name('Jane') }
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
- # rom.relation(:users) { |r| r.page(1) }.map_with(:presenter, :json_serializer)
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::Loaded]
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
- # # allow auto-mapping using registered mappers
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(name)
88
- if mappers.key?(name)
89
- commands[name].with(mappers: mappers[name])
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
- commands[name]
121
+ raise ArgumentError, "#{self.class}#command accepts a symbol or an array"
92
122
  end
93
123
  end
94
124
  end
@@ -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