rom 0.5.0 → 0.6.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -15
  3. data/.rubocop_todo.yml +28 -0
  4. data/.travis.yml +8 -1
  5. data/CHANGELOG.md +40 -0
  6. data/Gemfile +10 -2
  7. data/Guardfile +12 -10
  8. data/README.md +42 -43
  9. data/Rakefile +13 -23
  10. data/lib/rom.rb +19 -27
  11. data/lib/rom/command.rb +118 -0
  12. data/lib/rom/command_registry.rb +13 -27
  13. data/lib/rom/commands.rb +1 -59
  14. data/lib/rom/commands/abstract.rb +147 -0
  15. data/lib/rom/commands/composite.rb +47 -0
  16. data/lib/rom/commands/create.rb +2 -17
  17. data/lib/rom/commands/delete.rb +5 -25
  18. data/lib/rom/commands/result.rb +5 -5
  19. data/lib/rom/commands/update.rb +3 -27
  20. data/lib/rom/constants.rb +19 -0
  21. data/lib/rom/env.rb +85 -35
  22. data/lib/rom/global.rb +173 -42
  23. data/lib/rom/header.rb +5 -5
  24. data/lib/rom/header/attribute.rb +2 -2
  25. data/lib/rom/lint/enumerable_dataset.rb +52 -0
  26. data/lib/rom/lint/linter.rb +64 -0
  27. data/lib/rom/lint/repository.rb +78 -0
  28. data/lib/rom/lint/spec.rb +20 -0
  29. data/lib/rom/lint/test.rb +98 -0
  30. data/lib/rom/mapper.rb +32 -5
  31. data/lib/rom/mapper/attribute_dsl.rb +240 -0
  32. data/lib/rom/mapper/dsl.rb +100 -0
  33. data/lib/rom/mapper/model_dsl.rb +55 -0
  34. data/lib/rom/mapper_registry.rb +8 -1
  35. data/lib/rom/memory.rb +4 -0
  36. data/lib/rom/memory/commands.rb +46 -0
  37. data/lib/rom/memory/dataset.rb +72 -0
  38. data/lib/rom/memory/relation.rb +44 -0
  39. data/lib/rom/memory/repository.rb +62 -0
  40. data/lib/rom/memory/storage.rb +57 -0
  41. data/lib/rom/model_builder.rb +44 -5
  42. data/lib/rom/processor.rb +1 -1
  43. data/lib/rom/processor/transproc.rb +109 -16
  44. data/lib/rom/reader.rb +91 -39
  45. data/lib/rom/relation.rb +165 -26
  46. data/lib/rom/relation/composite.rb +132 -0
  47. data/lib/rom/relation/curried.rb +48 -0
  48. data/lib/rom/relation/lazy.rb +173 -0
  49. data/lib/rom/relation/loaded.rb +75 -0
  50. data/lib/rom/relation/registry_reader.rb +23 -0
  51. data/lib/rom/repository.rb +93 -34
  52. data/lib/rom/setup.rb +54 -98
  53. data/lib/rom/setup/finalize.rb +85 -76
  54. data/lib/rom/setup_dsl/command.rb +36 -0
  55. data/lib/rom/setup_dsl/command_dsl.rb +34 -0
  56. data/lib/rom/setup_dsl/mapper.rb +32 -0
  57. data/lib/rom/setup_dsl/mapper_dsl.rb +30 -0
  58. data/lib/rom/setup_dsl/relation.rb +21 -0
  59. data/lib/rom/setup_dsl/setup.rb +75 -0
  60. data/lib/rom/support/array_dataset.rb +38 -0
  61. data/lib/rom/support/class_builder.rb +44 -0
  62. data/lib/rom/support/class_macros.rb +56 -0
  63. data/lib/rom/support/data_proxy.rb +102 -0
  64. data/lib/rom/support/enumerable_dataset.rb +58 -0
  65. data/lib/rom/support/inflector.rb +73 -0
  66. data/lib/rom/support/options.rb +188 -0
  67. data/lib/rom/support/registry.rb +4 -8
  68. data/lib/rom/version.rb +1 -1
  69. data/rakelib/benchmark.rake +13 -0
  70. data/rakelib/mutant.rake +16 -0
  71. data/rakelib/rubocop.rake +18 -0
  72. data/rom.gemspec +4 -7
  73. data/spec/integration/commands/create_spec.rb +32 -24
  74. data/spec/integration/commands/delete_spec.rb +15 -7
  75. data/spec/integration/commands/update_spec.rb +13 -11
  76. data/spec/integration/mappers/deep_embedded_spec.rb +4 -11
  77. data/spec/integration/mappers/definition_dsl_spec.rb +31 -44
  78. data/spec/integration/mappers/embedded_spec.rb +9 -24
  79. data/spec/integration/mappers/group_spec.rb +22 -30
  80. data/spec/integration/mappers/prefixing_attributes_spec.rb +18 -23
  81. data/spec/integration/mappers/renaming_attributes_spec.rb +23 -38
  82. data/spec/integration/mappers/symbolizing_attributes_spec.rb +18 -24
  83. data/spec/integration/mappers/wrap_spec.rb +22 -30
  84. data/spec/integration/multi_repo_spec.rb +15 -37
  85. data/spec/integration/relations/reading_spec.rb +82 -14
  86. data/spec/integration/repositories/extending_relations_spec.rb +50 -0
  87. data/spec/integration/{adapters → repositories}/setting_logger_spec.rb +6 -5
  88. data/spec/integration/setup_spec.rb +59 -62
  89. data/spec/shared/enumerable_dataset.rb +49 -0
  90. data/spec/shared/one_behavior.rb +26 -0
  91. data/spec/shared/users_and_tasks.rb +11 -23
  92. data/spec/spec_helper.rb +16 -7
  93. data/spec/support/constant_leak_finder.rb +14 -0
  94. data/spec/test/memory_repository_lint_test.rb +27 -0
  95. data/spec/unit/rom/command_registry_spec.rb +44 -0
  96. data/spec/unit/rom/commands/result_spec.rb +14 -0
  97. data/spec/unit/rom/commands_spec.rb +174 -0
  98. data/spec/unit/rom/env_spec.rb +40 -7
  99. data/spec/unit/rom/global_spec.rb +14 -0
  100. data/spec/unit/rom/{mapper_builder_spec.rb → mapper/dsl_spec.rb} +52 -38
  101. data/spec/unit/rom/mapper_spec.rb +51 -10
  102. data/spec/unit/rom/{adapter/memory → memory}/dataset_spec.rb +6 -4
  103. data/spec/unit/rom/memory/repository_spec.rb +12 -0
  104. data/spec/unit/rom/memory/storage_spec.rb +45 -0
  105. data/spec/unit/rom/model_builder_spec.rb +4 -3
  106. data/spec/unit/rom/processor/transproc_spec.rb +1 -0
  107. data/spec/unit/rom/reader_spec.rb +97 -24
  108. data/spec/unit/rom/relation/composite_spec.rb +65 -0
  109. data/spec/unit/rom/relation/lazy_spec.rb +145 -0
  110. data/spec/unit/rom/relation/loaded_spec.rb +28 -0
  111. data/spec/unit/rom/relation_spec.rb +111 -6
  112. data/spec/unit/rom/repository_spec.rb +59 -9
  113. data/spec/unit/rom/setup_spec.rb +99 -11
  114. data/spec/unit/rom/support/array_dataset_spec.rb +59 -0
  115. data/spec/unit/rom/support/class_builder_spec.rb +42 -0
  116. data/spec/unit/rom/support/enumerable_dataset_spec.rb +17 -0
  117. data/spec/unit/rom/support/inflector_spec.rb +89 -0
  118. data/spec/unit/rom/support/options_spec.rb +119 -0
  119. metadata +74 -112
  120. data/lib/rom/adapter.rb +0 -191
  121. data/lib/rom/adapter/memory.rb +0 -32
  122. data/lib/rom/adapter/memory/commands.rb +0 -31
  123. data/lib/rom/adapter/memory/dataset.rb +0 -67
  124. data/lib/rom/adapter/memory/storage.rb +0 -26
  125. data/lib/rom/commands/with_options.rb +0 -18
  126. data/lib/rom/config.rb +0 -70
  127. data/lib/rom/mapper_builder.rb +0 -52
  128. data/lib/rom/mapper_builder/mapper_dsl.rb +0 -114
  129. data/lib/rom/mapper_builder/model_dsl.rb +0 -29
  130. data/lib/rom/reader_builder.rb +0 -48
  131. data/lib/rom/relation_builder.rb +0 -62
  132. data/lib/rom/setup/base_relation_dsl.rb +0 -46
  133. data/lib/rom/setup/command_dsl.rb +0 -46
  134. data/lib/rom/setup/mapper_dsl.rb +0 -19
  135. data/lib/rom/setup/relation_dsl.rb +0 -20
  136. data/lib/rom/setup/schema_dsl.rb +0 -33
  137. data/spec/integration/adapters/extending_relations_spec.rb +0 -41
  138. data/spec/integration/commands/try_spec.rb +0 -27
  139. data/spec/integration/schema_spec.rb +0 -77
  140. data/spec/unit/config_spec.rb +0 -60
  141. data/spec/unit/rom/adapter_spec.rb +0 -79
  142. data/spec/unit/rom_spec.rb +0 -14
@@ -1,8 +1,31 @@
1
1
  module ROM
2
- # @api private
2
+ # Model builders can be used to build model classes for mappers
3
+ #
4
+ # This is used when you define a mapper and setup a model using :name option.
5
+ #
6
+ # @example
7
+ # # this will define User model for you
8
+ # class UserMapper < ROM::Mapper
9
+ # model name: 'User'
10
+ # attribute :id
11
+ # attribute :name
12
+ # end
13
+ #
14
+ # @private
3
15
  class ModelBuilder
4
- attr_reader :options, :const_name, :namespace, :klass
16
+ include Options
5
17
 
18
+ option :name, reader: true
19
+
20
+ attr_reader :const_name, :namespace, :klass
21
+
22
+ # Return model builder subclass based on type
23
+ #
24
+ # @param [Symbol] type
25
+ #
26
+ # @return [Class]
27
+ #
28
+ # @api private
6
29
  def self.[](type)
7
30
  case type
8
31
  when :poro then PORO
@@ -11,14 +34,19 @@ module ROM
11
34
  end
12
35
  end
13
36
 
37
+ # Build a model class
38
+ #
39
+ # @return [Class]
40
+ #
41
+ # @api private
14
42
  def self.call(*args)
15
43
  new(*args).call
16
44
  end
17
45
 
46
+ # @api private
18
47
  def initialize(options = {})
19
- @options = options
48
+ super
20
49
 
21
- name = options[:name]
22
50
  if name
23
51
  parts = name.split('::')
24
52
 
@@ -26,23 +54,34 @@ module ROM
26
54
 
27
55
  @namespace =
28
56
  if parts.any?
29
- Inflecto.constantize(parts.join('::'))
57
+ Inflector.constantize(parts.join('::'))
30
58
  else
31
59
  Object
32
60
  end
33
61
  end
34
62
  end
35
63
 
64
+ # Define a model class constant
65
+ #
66
+ # @api private
36
67
  def define_const
37
68
  namespace.const_set(const_name, klass)
38
69
  end
39
70
 
71
+ # Build a model class supporting specific attributes
72
+ #
73
+ # @return [Class]
74
+ #
75
+ # @api private
40
76
  def call(attrs)
41
77
  define_class(attrs)
42
78
  define_const if const_name
43
79
  @klass
44
80
  end
45
81
 
82
+ # PORO model class builder
83
+ #
84
+ # @private
46
85
  class PORO < ModelBuilder
47
86
  def define_class(attrs)
48
87
  @klass = Class.new
data/lib/rom/processor.rb CHANGED
@@ -5,7 +5,7 @@ module ROM
5
5
  #
6
6
  # Every ROM processor should inherit from this class
7
7
  #
8
- # @public
8
+ # @api public
9
9
  class Processor
10
10
  # Hook used to auto-register a processor class
11
11
  #
@@ -4,66 +4,146 @@ require 'rom/processor'
4
4
 
5
5
  module ROM
6
6
  class Processor
7
+ # Data mapping transformer builder using Transproc
8
+ #
9
+ # This builds a transproc function that is used to map a whole relation
10
+ #
11
+ # @see https://github.com/solnic/transproc too
12
+ #
13
+ # @private
7
14
  class Transproc < Processor
8
15
  include ::Transproc::Composer
9
16
 
10
- attr_reader :header, :model, :mapping, :tuple_proc
17
+ # @return [Header] header from a mapper
18
+ #
19
+ # @api private
20
+ attr_reader :header
11
21
 
22
+ # @return [Class] model class from a mapper
23
+ #
24
+ # @api private
25
+ attr_reader :model
26
+
27
+ # @return [Hash] header's attribute mapping
28
+ #
29
+ # @api private
30
+ attr_reader :mapping
31
+
32
+ # @return [Proc] row-processing proc
33
+ #
34
+ # @api private
35
+ attr_reader :row_proc
36
+
37
+ # Default no-op row_proc
12
38
  EMPTY_FN = -> tuple { tuple }.freeze
13
39
 
40
+ # Build a transproc function from the header
41
+ #
42
+ # @param [ROM::Header] header
43
+ #
44
+ # @return [Transproc::Function]
45
+ #
46
+ # @api private
14
47
  def self.build(header)
15
48
  new(header).to_transproc
16
49
  end
17
50
 
51
+ # @api private
18
52
  def initialize(header)
19
53
  @header = header
20
54
  @model = header.model
21
55
  @mapping = header.mapping
22
- initialize_tuple_proc
56
+ initialize_row_proc
23
57
  end
24
58
 
59
+ # Coerce mapper header to a transproc data mapping function
60
+ #
61
+ # @return [Transproc::Function]
62
+ #
63
+ # @api private
25
64
  def to_transproc
26
65
  compose(EMPTY_FN) do |ops|
27
66
  ops << header.groups.map { |attr| visit_group(attr, true) }
28
- ops << t(:map_array!, tuple_proc) if tuple_proc
67
+ ops << t(:map_array, row_proc) if row_proc
29
68
  end
30
69
  end
31
70
 
32
71
  private
33
72
 
73
+ # Visit an attribute from the header
74
+ #
75
+ # This forwards to a specialized visitor based on the attribute type
76
+ #
77
+ # @param [Header::Attribute] attribute
78
+ #
79
+ # @api private
34
80
  def visit(attribute)
35
81
  type = attribute.class.name.split('::').last.downcase
36
82
  send("visit_#{type}", attribute)
37
83
  end
38
84
 
85
+ # Visit plain attribute
86
+ #
87
+ # If it's a typed attribute a coercion transformation is added
88
+ #
89
+ # @param [Header::Attribute] attribute
90
+ #
91
+ # @api private
39
92
  def visit_attribute(attribute)
40
93
  if attribute.typed?
41
- t(:map_key!, attribute.name, t(:"to_#{attribute.type}"))
94
+ t(:map_key, attribute.name, t(:"to_#{attribute.type}"))
42
95
  end
43
96
  end
44
97
 
98
+ # Visit hash attribute
99
+ #
100
+ # @param [Header::Attribute::Hash] attribute
101
+ #
102
+ # @api private
45
103
  def visit_hash(attribute)
46
- with_tuple_proc(attribute) do |tuple_proc|
47
- t(:map_key!, attribute.name, tuple_proc)
104
+ with_row_proc(attribute) do |row_proc|
105
+ t(:map_key, attribute.name, row_proc)
48
106
  end
49
107
  end
50
108
 
109
+ # Visit array attribute
110
+ #
111
+ # @param [Header::Attribute::Array] attribute
112
+ #
113
+ # @api private
51
114
  def visit_array(attribute)
52
- with_tuple_proc(attribute) do |tuple_proc|
53
- t(:map_key!, attribute.name, t(:map_array!, tuple_proc))
115
+ with_row_proc(attribute) do |row_proc|
116
+ t(:map_key, attribute.name, t(:map_array, row_proc))
54
117
  end
55
118
  end
56
119
 
120
+ # Visit wrapped hash attribute
121
+ #
122
+ # :nest transformation is added to handle wrapping
123
+ #
124
+ # @param [Header::Attribute::Wrap] attribute
125
+ #
126
+ # @api private
57
127
  def visit_wrap(attribute)
58
128
  name = attribute.name
59
129
  keys = attribute.tuple_keys
60
130
 
61
131
  compose do |ops|
62
- ops << t(:nest!, name, keys)
132
+ ops << t(:nest, name, keys)
63
133
  ops << visit_hash(attribute)
64
134
  end
65
135
  end
66
136
 
137
+ # Visit group hash attribute
138
+ #
139
+ # :group transformation is added to handle grouping during preprocessing.
140
+ # Otherwise we simply use array visitor for the attribute.
141
+ #
142
+ # @param [Header::Attribute::Group] attribute
143
+ # @param [Boolean] preprocess true if we are building a relation preprocessing
144
+ # function that is applied to the whole relation
145
+ #
146
+ # @api private
67
147
  def visit_group(attribute, preprocess = false)
68
148
  if preprocess
69
149
  name = attribute.name
@@ -76,7 +156,7 @@ module ROM
76
156
  ops << t(:group, name, keys)
77
157
 
78
158
  ops << other.map { |attr|
79
- t(:map_array!, t(:map_key!, name, visit_group(attr, true)))
159
+ t(:map_array, t(:map_key, name, visit_group(attr, true)))
80
160
  }
81
161
  end
82
162
  else
@@ -84,19 +164,32 @@ module ROM
84
164
  end
85
165
  end
86
166
 
87
- def initialize_tuple_proc
88
- @tuple_proc = compose do |ops|
89
- ops << t(:map_hash!, mapping) if header.aliased?
167
+ # Build row_proc
168
+ #
169
+ # This transproc function is applied to each row in a dataset
170
+ #
171
+ # @api private
172
+ def initialize_row_proc
173
+ @row_proc = compose do |ops|
174
+ ops << t(:map_hash, mapping) if header.aliased?
90
175
  ops << header.map { |attr| visit(attr) }
91
176
  ops << t(-> tuple { model.new(tuple) }) if model
92
177
  end
93
178
  end
94
179
 
95
- def with_tuple_proc(attribute)
96
- tuple_proc = new(attribute.header).tuple_proc
97
- yield(tuple_proc) if tuple_proc
180
+ # Yield row proc for a given attribute if any
181
+ #
182
+ # @param [Header::Attribute] attribute
183
+ #
184
+ # @api private
185
+ def with_row_proc(attribute)
186
+ row_proc = new(attribute.header).row_proc
187
+ yield(row_proc) if row_proc
98
188
  end
99
189
 
190
+ # Return a new instance of the processor
191
+ #
192
+ # @api private
100
193
  def new(*args)
101
194
  self.class.new(*args)
102
195
  end
data/lib/rom/reader.rb CHANGED
@@ -5,11 +5,16 @@ module ROM
5
5
  #
6
6
  # @api public
7
7
  class Reader
8
- MapperMissingError = Class.new(StandardError)
9
-
10
8
  include Enumerable
11
9
  include Equalizer.new(:path, :relation, :mapper)
12
10
 
11
+ # Map relation to an array using a mapper
12
+ #
13
+ # @return [Array]
14
+ #
15
+ # @api public
16
+ alias_method :to_ary, :to_a
17
+
13
18
  # @return [String] access path used to read a relation
14
19
  #
15
20
  # @api private
@@ -30,58 +35,55 @@ module ROM
30
35
  # @api private
31
36
  attr_reader :mapper
32
37
 
33
- # Build a reader subclass for the relation and instantiate it
34
- #
35
- # This method defines public methods on the class narrowing down data access
36
- # only to the methods exposed by a given relation
38
+ # Builds a reader instance for the provided relation
37
39
  #
38
40
  # @param [Symbol] name of the root relation
39
41
  # @param [Relation] relation that the reader will use
40
- # @param [MapperRegistry] registry of mappers
41
- # @param [Array<Symbol>] a list of method names exposed by the relation
42
+ # @param [MapperRegistry] mappers registry of mappers
43
+ # @param [Array<Symbol>] method_names a list of method names exposed by the relation
42
44
  #
43
45
  # @return [Reader]
44
46
  #
45
47
  # @api private
46
48
  def self.build(name, relation, mappers, method_names = [])
47
- klass = Class.new(self)
48
-
49
- klass_name = "#{self.name}[#{Inflecto.camelize(relation.name)}]"
50
-
51
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
52
- def self.name
53
- #{klass_name.inspect}
54
- end
55
-
56
- def self.inspect
57
- name
58
- end
49
+ klass = build_class(relation, method_names)
50
+ klass.new(name, relation, mappers)
51
+ end
59
52
 
60
- def self.to_s
61
- name
53
+ # Build a reader subclass for the relation
54
+ #
55
+ # This method defines public methods on the class narrowing down data access
56
+ # only to the methods exposed by a given relation
57
+ #
58
+ # @param [Relation] relation that the reader will use
59
+ # @param [Array<Symbol>] method_names a list of method names exposed by the relation
60
+ #
61
+ # @return [Class]
62
+ #
63
+ # @api private
64
+ def self.build_class(relation, method_names)
65
+ klass_name = "#{Reader.name}[#{Inflector.camelize(relation.name)}]"
66
+
67
+ ClassBuilder.new(name: klass_name, parent: Reader).call do |klass|
68
+ method_names.each do |method_name|
69
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
70
+ def #{method_name}(*args, &block)
71
+ new_relation = relation.send(#{method_name.inspect}, *args, &block)
72
+ self.class.new(
73
+ new_path(#{method_name.to_s.inspect}), new_relation, mappers
74
+ )
75
+ end
76
+ RUBY
62
77
  end
63
- RUBY
64
-
65
- method_names.each do |method_name|
66
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
67
- def #{method_name}(*args, &block)
68
- new_relation = relation.send(#{method_name.inspect}, *args, &block)
69
- self.class.new(
70
- new_path(#{method_name.to_s.inspect}), new_relation, mappers
71
- )
72
- end
73
- RUBY
74
78
  end
75
-
76
- klass.new(name, relation, mappers)
77
79
  end
78
80
 
79
81
  # @api private
80
- def initialize(path, relation, mappers = {})
82
+ def initialize(path, relation, mappers, mapper = nil)
81
83
  @path = path.to_s
82
84
  @relation = relation
83
85
  @mappers = mappers
84
- @mapper = mappers.by_path(@path) || raise(MapperMissingError, path)
86
+ @mapper = mapper || mappers.by_path(@path)
85
87
  end
86
88
 
87
89
  # @api private
@@ -101,13 +103,63 @@ module ROM
101
103
  #
102
104
  # @api public
103
105
  def each
104
- mapper.process(relation) { |tuple| yield(tuple) }
106
+ mapper.call(relation).each { |tuple| yield(tuple) }
107
+ end
108
+
109
+ # Returns a single tuple from the relation if there is one.
110
+ #
111
+ # @raise [ROM::TupleCountMismatchError] if the relation contains more than
112
+ # one tuple
113
+ #
114
+ # @api public
115
+ def one
116
+ if relation.count > 1
117
+ raise(
118
+ TupleCountMismatchError,
119
+ 'The relation consists of more than one tuple'
120
+ )
121
+ else
122
+ mapper.call(relation).first
123
+ end
124
+ end
125
+
126
+ # Like [one], but additionally raises an error if the relation is empty.
127
+ #
128
+ # @raise [ROM::TupleCountMismatchError] if the relation does not contain
129
+ # exactly one tuple
130
+ #
131
+ # @api public
132
+ def one!
133
+ one || raise(
134
+ TupleCountMismatchError,
135
+ 'The relation does not contain any tuples'
136
+ )
137
+ end
138
+
139
+ # Map tuples using a specific mapper if name is provided
140
+ #
141
+ # Defaults to Enumerable#map behavior
142
+ #
143
+ # @example
144
+ #
145
+ # rom.read(:users).map(:my_mapper_name)
146
+ # rom.read(:users).map { |user| ... }
147
+ #
148
+ # @return [Array,Reader]
149
+ #
150
+ # @api public
151
+ def map(*args)
152
+ if args.any?
153
+ mappers[args[0]].call(relation)
154
+ else
155
+ super
156
+ end
105
157
  end
106
158
 
107
159
  private
108
160
 
109
161
  # @api private
110
- def method_missing(name)
162
+ def method_missing(name, *)
111
163
  raise(
112
164
  NoRelationError,
113
165
  "undefined relation #{name.inspect} within #{path.inspect}"