rom 1.0.0 → 2.0.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +38 -0
  5. data/Gemfile +2 -14
  6. data/README.md +11 -17
  7. data/lib/rom.rb +2 -0
  8. data/lib/rom/association_set.rb +26 -0
  9. data/lib/rom/command.rb +50 -45
  10. data/lib/rom/command_registry.rb +26 -3
  11. data/lib/rom/commands/class_interface.rb +52 -19
  12. data/lib/rom/commands/composite.rb +5 -0
  13. data/lib/rom/commands/delete.rb +1 -5
  14. data/lib/rom/commands/graph.rb +11 -0
  15. data/lib/rom/commands/lazy.rb +2 -0
  16. data/lib/rom/commands/update.rb +1 -5
  17. data/lib/rom/configuration.rb +2 -0
  18. data/lib/rom/container.rb +3 -3
  19. data/lib/rom/global.rb +1 -23
  20. data/lib/rom/memory/commands.rb +2 -0
  21. data/lib/rom/memory/relation.rb +3 -0
  22. data/lib/rom/memory/storage.rb +4 -7
  23. data/lib/rom/memory/types.rb +9 -0
  24. data/lib/rom/pipeline.rb +26 -12
  25. data/lib/rom/plugin_registry.rb +2 -2
  26. data/lib/rom/plugins/command/schema.rb +26 -0
  27. data/lib/rom/plugins/configuration/configuration_dsl.rb +2 -1
  28. data/lib/rom/plugins/relation/key_inference.rb +18 -3
  29. data/lib/rom/plugins/relation/registry_reader.rb +3 -1
  30. data/lib/rom/plugins/relation/view.rb +11 -6
  31. data/lib/rom/relation.rb +76 -16
  32. data/lib/rom/relation/class_interface.rb +44 -3
  33. data/lib/rom/relation/curried.rb +13 -4
  34. data/lib/rom/relation/graph.rb +15 -5
  35. data/lib/rom/relation/loaded.rb +42 -6
  36. data/lib/rom/relation/name.rb +102 -0
  37. data/lib/rom/relation_registry.rb +5 -0
  38. data/lib/rom/schema.rb +87 -0
  39. data/lib/rom/schema/dsl.rb +58 -0
  40. data/lib/rom/setup/auto_registration.rb +2 -2
  41. data/lib/rom/setup/finalize.rb +5 -5
  42. data/lib/rom/setup/finalize/{commands.rb → finalize_commands.rb} +2 -22
  43. data/lib/rom/setup/finalize/{mappers.rb → finalize_mappers.rb} +0 -0
  44. data/lib/rom/setup/finalize/finalize_relations.rb +60 -0
  45. data/lib/rom/types.rb +18 -0
  46. data/lib/rom/version.rb +1 -1
  47. data/log/.gitkeep +0 -0
  48. data/rom.gemspec +4 -2
  49. data/spec/integration/command_registry_spec.rb +13 -0
  50. data/spec/integration/commands/delete_spec.rb +0 -17
  51. data/spec/integration/commands/graph_builder_spec.rb +1 -1
  52. data/spec/integration/commands/graph_spec.rb +1 -1
  53. data/spec/integration/commands/update_spec.rb +0 -19
  54. data/spec/integration/commands_spec.rb +10 -3
  55. data/spec/integration/multi_repo_spec.rb +1 -1
  56. data/spec/integration/relations/default_dataset_spec.rb +27 -4
  57. data/spec/integration/setup_spec.rb +1 -4
  58. data/spec/shared/command_behavior.rb +17 -7
  59. data/spec/shared/container.rb +2 -2
  60. data/spec/shared/gateway_only.rb +1 -1
  61. data/spec/spec_helper.rb +5 -6
  62. data/spec/unit/rom/association_set_spec.rb +23 -0
  63. data/spec/unit/rom/auto_registration_spec.rb +1 -1
  64. data/spec/unit/rom/commands/lazy_spec.rb +8 -0
  65. data/spec/unit/rom/commands_spec.rb +45 -7
  66. data/spec/unit/rom/configurable_spec.rb +1 -1
  67. data/spec/unit/rom/container_spec.rb +6 -0
  68. data/spec/unit/rom/create_container_spec.rb +1 -1
  69. data/spec/unit/rom/environment_spec.rb +1 -1
  70. data/spec/unit/rom/memory/commands_spec.rb +43 -0
  71. data/spec/unit/rom/plugins/relation/key_inference_spec.rb +70 -12
  72. data/spec/unit/rom/plugins/relation/view_spec.rb +4 -0
  73. data/spec/unit/rom/relation/graph_spec.rb +10 -0
  74. data/spec/unit/rom/relation/lazy_spec.rb +3 -3
  75. data/spec/unit/rom/relation/loaded_spec.rb +15 -0
  76. data/spec/unit/rom/relation/name_spec.rb +51 -0
  77. data/spec/unit/rom/relation/schema_spec.rb +117 -0
  78. data/spec/unit/rom/relation_spec.rb +37 -7
  79. data/spec/unit/rom/schema_spec.rb +10 -0
  80. metadata +51 -12
  81. data/lib/rom/setup/finalize/relations.rb +0 -53
  82. data/spec/unit/rom/global_spec.rb +0 -18
  83. data/spec/unit/rom/registry_spec.rb +0 -38
@@ -1,6 +1,8 @@
1
1
  module ROM
2
2
  module Plugins
3
3
  module Relation
4
+ EMPTY_REGISTRY = RelationRegistry.new.freeze
5
+
4
6
  # Allows relations to access all other relations through registry
5
7
  #
6
8
  # For now this plugin is always enabled
@@ -10,7 +12,7 @@ module ROM
10
12
  # @api private
11
13
  def self.included(klass)
12
14
  super
13
- klass.option :__registry__, type: Hash, default: EMPTY_HASH, reader: true
15
+ klass.option :__registry__, type: RelationRegistry, default: EMPTY_REGISTRY, reader: true
14
16
  end
15
17
 
16
18
  # @api private
@@ -11,6 +11,7 @@ module ROM
11
11
  extend ClassInterface
12
12
 
13
13
  option :view, reader: true
14
+ option :attributes
14
15
 
15
16
  def self.attributes
16
17
  @__attributes__ ||= {}
@@ -27,13 +28,17 @@ module ROM
27
28
  #
28
29
  # @api private
29
30
  def attributes(view_name = view)
30
- header = self.class.attributes
31
- .fetch(view_name, self.class.attributes.fetch(:base))
32
-
33
- if header.is_a?(Proc)
34
- instance_exec(&header)
31
+ if options.key?(:attributes)
32
+ options[:attributes]
35
33
  else
36
- header
34
+ header = self.class.attributes
35
+ .fetch(view_name, self.class.attributes.fetch(:base))
36
+
37
+ if header.is_a?(Proc)
38
+ instance_exec(&header)
39
+ else
40
+ header
41
+ end
37
42
  end
38
43
  end
39
44
 
@@ -8,6 +8,10 @@ require 'rom/relation/curried'
8
8
  require 'rom/relation/composite'
9
9
  require 'rom/relation/graph'
10
10
  require 'rom/relation/materializable'
11
+ require 'rom/association_set'
12
+
13
+ require 'rom/types'
14
+ require 'rom/schema'
11
15
 
12
16
  module ROM
13
17
  # Base relation class
@@ -32,36 +36,63 @@ module ROM
32
36
  include Materializable
33
37
  include Pipeline
34
38
 
39
+ # @!attribute [r] mappers
40
+ # @return [MapperRegistry] an optional mapper registry (empty by default)
35
41
  option :mappers, reader: true, default: proc { MapperRegistry.new }
36
42
 
37
- # Dataset used by the relation
43
+ # @!attribute [r] schema_hash
44
+ # @return [Object#[]] tuple processing function, uses schema or defaults to Hash[]
45
+ # @api private
46
+ option :schema_hash, reader: true, default: -> relation {
47
+ relation.schema? ? Types::Coercible::Hash.schema(relation.schema.to_h) : Hash
48
+ }
49
+
50
+ # @!attribute [r] associations
51
+ # @return [AssociationSet] Schema's association set (empty by default)
52
+ option :associations, reader: true, default: -> rel {
53
+ rel.schema? ? rel.schema.associations : Schema::EMPTY_ASSOCIATION_SET
54
+ }
55
+
56
+ # @!attribute [r] dataset
57
+ # @return [Object] dataset used by the relation provided by relation's gateway
58
+ # @api public
59
+ attr_reader :dataset
60
+
61
+ # @!attribute [r] schema
62
+ # @return [Schema] returns relation schema object (if defined)
63
+ # @api public
64
+ attr_reader :schema
65
+
66
+ # Initializes a relation object
38
67
  #
39
- # This object is provided by the gateway during the setup
68
+ # @param dataset [Object]
40
69
  #
41
- # @return [Object]
70
+ # @param options [Hash]
71
+ # @option :mappers [MapperRegistry]
72
+ # @option :schema_hash [#[]]
73
+ # @option :associations [AssociationSet]
42
74
  #
43
- # @api private
44
- attr_reader :dataset
45
-
46
- # @api private
75
+ # @api public
47
76
  def initialize(dataset, options = EMPTY_HASH)
48
77
  @dataset = dataset
78
+ @schema = self.class.schema
49
79
  super
50
80
  end
51
81
 
52
- # Yield dataset tuples
82
+ # Yields relation tuples
53
83
  #
54
84
  # @yield [Hash]
85
+ # @return [Enumerator] if block is not provided
55
86
  #
56
- # @api private
87
+ # @api public
57
88
  def each(&block)
58
89
  return to_enum unless block
59
90
  dataset.each { |tuple| yield(tuple) }
60
91
  end
61
92
 
62
- # Eager load other relation(s) for this relation
93
+ # Composes with other relations
63
94
  #
64
- # @param [Array<Relation>] others The other relation(s) to eager load
95
+ # @param *others [Array<Relation>] The other relation(s) to compose with
65
96
  #
66
97
  # @return [Relation::Graph]
67
98
  #
@@ -70,7 +101,7 @@ module ROM
70
101
  Graph.build(self, others)
71
102
  end
72
103
 
73
- # Load relation
104
+ # Loads relation
74
105
  #
75
106
  # @return [Relation::Loaded]
76
107
  #
@@ -79,7 +110,7 @@ module ROM
79
110
  Loaded.new(self)
80
111
  end
81
112
 
82
- # Materialize a relation into an array
113
+ # Materializes a relation into an array
83
114
  #
84
115
  # @return [Array<Hash>]
85
116
  #
@@ -88,7 +119,7 @@ module ROM
88
119
  to_enum.to_a
89
120
  end
90
121
 
91
- # Return if this relation is curried
122
+ # Returns if this relation is curried
92
123
  #
93
124
  # @return [false]
94
125
  #
@@ -97,9 +128,33 @@ module ROM
97
128
  false
98
129
  end
99
130
 
131
+ # Returns if this relation is a graph
132
+ #
133
+ # @return [false]
134
+ #
100
135
  # @api private
101
- def with(options)
102
- __new__(dataset, options)
136
+ def graph?
137
+ false
138
+ end
139
+
140
+ # Returns true if a relation has schema defined
141
+ #
142
+ # @return [TrueClass, FalseClass]
143
+ #
144
+ # @api private
145
+ def schema?
146
+ ! schema.nil?
147
+ end
148
+
149
+ # Returns a new instance with the same dataset but new options
150
+ #
151
+ # @param new_options [Hash]
152
+ #
153
+ # @return [Relation]
154
+ #
155
+ # @api private
156
+ def with(new_options)
157
+ __new__(dataset, options.merge(new_options))
103
158
  end
104
159
 
105
160
  private
@@ -108,5 +163,10 @@ module ROM
108
163
  def __new__(dataset, new_opts = EMPTY_HASH)
109
164
  self.class.new(dataset, options.merge(new_opts))
110
165
  end
166
+
167
+ # @api private
168
+ def composite_class
169
+ Relation::Composite
170
+ end
111
171
  end
112
172
  end
@@ -2,6 +2,8 @@ require 'set'
2
2
 
3
3
  require 'rom/support/auto_curry'
4
4
  require 'rom/relation/curried'
5
+ require 'rom/relation/name'
6
+ require 'rom/schema'
5
7
 
6
8
  module ROM
7
9
  class Relation
@@ -30,9 +32,11 @@ module ROM
30
32
  klass.class_eval do
31
33
  use :registry_reader
32
34
 
33
- defines :gateway, :dataset, :dataset_proc, :register_as
35
+ defines :gateway, :dataset, :dataset_proc, :register_as, :schema_dsl, :schema_inferrer
34
36
 
35
37
  gateway :default
38
+ schema_dsl Schema::DSL
39
+ schema_inferrer nil
36
40
 
37
41
  dataset default_name
38
42
 
@@ -93,7 +97,7 @@ module ROM
93
97
 
94
98
  # @api private
95
99
  def initialize(dataset, options = EMPTY_HASH)
96
- @name = self.class.dataset
100
+ @name = Name.new(self.class.register_as, self.class.dataset)
97
101
  super
98
102
  end
99
103
 
@@ -125,6 +129,43 @@ module ROM
125
129
  raise AdapterNotPresentError.new(adapter, :relation)
126
130
  end
127
131
 
132
+ # Specify canonical schema for a relation
133
+ #
134
+ # With a schema defined commands will set up a type-safe input handler
135
+ # automatically
136
+ #
137
+ # @example
138
+ # class Users < ROM::Relation[:sql]
139
+ # schema do
140
+ # attribute :id, Types::Serial
141
+ # attribute :name, Types::String
142
+ # end
143
+ # end
144
+ #
145
+ # # access schema
146
+ # Users.schema
147
+ #
148
+ # @return [Schema]
149
+ #
150
+ # @param [Symbol] dataset An optional dataset name
151
+ # @param [Boolean] infer Whether to do an automatic schema inferring
152
+ #
153
+ # @api public
154
+ def schema(dataset = nil, infer: false, &block)
155
+ if defined?(@schema)
156
+ @schema
157
+ elsif block || infer
158
+ self.dataset(dataset) if dataset
159
+ self.register_as(self.dataset) unless register_as
160
+
161
+ name = Name[register_as, self.dataset]
162
+ inferrer = infer ? schema_inferrer : nil
163
+ dsl = schema_dsl.new(name, inferrer, &block)
164
+
165
+ @schema = dsl.call
166
+ end
167
+ end
168
+
128
169
  # Dynamically define a method that will forward to the dataset and wrap
129
170
  # response in the relation itself
130
171
  #
@@ -175,7 +216,7 @@ module ROM
175
216
  ancestor_methods = ancestors.reject { |klass| klass == self }
176
217
  .map(&:instance_methods).flatten
177
218
 
178
- instance_methods - ancestor_methods
219
+ instance_methods - ancestor_methods + auto_curried_methods
179
220
  end
180
221
 
181
222
  # Hook to finalize a relation after its instance was created
@@ -1,6 +1,7 @@
1
1
  require 'rom/support/options'
2
2
 
3
3
  require 'rom/pipeline'
4
+ require 'rom/relation/name'
4
5
  require 'rom/relation/materializable'
5
6
 
6
7
  module ROM
@@ -10,15 +11,18 @@ module ROM
10
11
  include Materializable
11
12
  include Pipeline
12
13
 
13
- option :name, type: Symbol, reader: true
14
+ option :name, type: Symbol
14
15
  option :arity, type: Integer, reader: true, default: -1
15
16
  option :curry_args, type: Array, reader: true, default: EMPTY_ARRAY
16
17
 
17
18
  attr_reader :relation
18
19
 
20
+ attr_reader :name
21
+
19
22
  # @api private
20
23
  def initialize(relation, options = EMPTY_HASH)
21
24
  @relation = relation
25
+ @name = relation.name.with(options[:name])
22
26
  super
23
27
  end
24
28
 
@@ -33,7 +37,7 @@ module ROM
33
37
  all_args = curry_args + args
34
38
 
35
39
  if arity == all_args.size
36
- Loaded.new(relation.__send__(name, *all_args))
40
+ Loaded.new(relation.__send__(name.relation, *all_args))
37
41
  else
38
42
  __new__(relation, curry_args: all_args)
39
43
  end
@@ -47,7 +51,7 @@ module ROM
47
51
  def to_a
48
52
  raise(
49
53
  ArgumentError,
50
- "#{relation.class}##{name} arity is #{arity} " \
54
+ "#{relation.class}##{name.relation} arity is #{arity} " \
51
55
  "(#{curry_args.size} args given)"
52
56
  )
53
57
  end
@@ -64,7 +68,7 @@ module ROM
64
68
 
65
69
  # @api private
66
70
  def respond_to_missing?(name, include_private = false)
67
- super || relation.respond_to?(name)
71
+ super || relation.respond_to?(name, include_private)
68
72
  end
69
73
 
70
74
  private
@@ -74,6 +78,11 @@ module ROM
74
78
  Curried.new(relation, options.merge(new_opts))
75
79
  end
76
80
 
81
+ # @api private
82
+ def composite_class
83
+ Relation::Composite
84
+ end
85
+
77
86
  # @api private
78
87
  def method_missing(meth, *args, &block)
79
88
  if relation.respond_to?(meth)
@@ -5,11 +5,9 @@ require 'rom/pipeline'
5
5
 
6
6
  module ROM
7
7
  class Relation
8
- # Load a relation with its associations
8
+ # Compose relations using join-keys
9
9
  #
10
10
  # @example
11
- # ROM.setup(:memory)
12
- #
13
11
  # class Users < ROM::Relation[:memory]
14
12
  # end
15
13
  #
@@ -19,8 +17,6 @@ module ROM
19
17
  # end
20
18
  # end
21
19
  #
22
- # rom = ROM.finalize.env
23
- #
24
20
  # rom.relations[:users] << { name: 'Jane' }
25
21
  # rom.relations[:tasks] << { user: 'Jane', title: 'Do something' }
26
22
  #
@@ -65,6 +61,15 @@ module ROM
65
61
  @nodes = nodes
66
62
  end
67
63
 
64
+ # Return if this is a graph relation
65
+ #
66
+ # @return [true]
67
+ #
68
+ # @api private
69
+ def graph?
70
+ true
71
+ end
72
+
68
73
  # Combine this graph with more nodes
69
74
  #
70
75
  # @param [Array<Relation::Lazy>]
@@ -100,6 +105,11 @@ module ROM
100
105
  def decorate?(other)
101
106
  super || other.is_a?(Curried)
102
107
  end
108
+
109
+ # @api private
110
+ def composite_class
111
+ Relation::Composite
112
+ end
103
113
  end
104
114
  end
105
115
  end
@@ -40,12 +40,7 @@ module ROM
40
40
  # @api public
41
41
  def each(&block)
42
42
  return to_enum unless block
43
- collection.each { |object| yield(object) }
44
- end
45
-
46
- # @api public
47
- def new(collection)
48
- self.class.new(source, collection)
43
+ collection.each { |tuple| yield(tuple) }
49
44
  end
50
45
 
51
46
  # Returns a single tuple from the relation if there is one.
@@ -77,6 +72,47 @@ module ROM
77
72
  'The relation does not contain any tuples'
78
73
  )
79
74
  end
75
+
76
+ # Return a list of values under provided key
77
+ #
78
+ # @example
79
+ # all_users = rom.relations[:users].call
80
+ # all_users.pluck(:name)
81
+ # # ["Jane", "Joe"]
82
+ #
83
+ # @param [Symbol] key The key name
84
+ #
85
+ # @return [Array]
86
+ # @raises KeyError when provided key doesn't exist in any of the tuples
87
+ #
88
+ # @api public
89
+ def pluck(key)
90
+ map { |tuple| tuple.fetch(key) }
91
+ end
92
+
93
+ # Pluck primary key values
94
+ #
95
+ # This method *may not work* with adapters that don't provide relations
96
+ # that have primary key configured
97
+ #
98
+ # @example
99
+ # users = rom.relations[:users].call
100
+ # users.primary_keys
101
+ # # [1, 2, 3]
102
+ #
103
+ # @return [Array]
104
+ #
105
+ # @api public
106
+ def primary_keys
107
+ pluck(source.primary_key)
108
+ end
109
+
110
+ # Return a loaded relation with a new collection
111
+ #
112
+ # @api public
113
+ def new(collection)
114
+ self.class.new(source, collection)
115
+ end
80
116
  end
81
117
  end
82
118
  end