rom 1.0.0 → 2.0.0

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