rom 0.4.2 → 0.5.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +81 -0
  3. data/.travis.yml +2 -1
  4. data/CHANGELOG.md +41 -0
  5. data/Gemfile +12 -8
  6. data/Guardfile +17 -11
  7. data/README.md +7 -7
  8. data/Rakefile +29 -0
  9. data/lib/rom.rb +9 -66
  10. data/lib/rom/adapter.rb +45 -12
  11. data/lib/rom/adapter/memory.rb +0 -4
  12. data/lib/rom/adapter/memory/commands.rb +0 -10
  13. data/lib/rom/adapter/memory/dataset.rb +18 -6
  14. data/lib/rom/adapter/memory/storage.rb +0 -3
  15. data/lib/rom/command_registry.rb +24 -43
  16. data/lib/rom/commands.rb +5 -6
  17. data/lib/rom/commands/create.rb +5 -5
  18. data/lib/rom/commands/delete.rb +8 -6
  19. data/lib/rom/commands/result.rb +82 -0
  20. data/lib/rom/commands/update.rb +5 -4
  21. data/lib/rom/commands/with_options.rb +1 -4
  22. data/lib/rom/config.rb +70 -0
  23. data/lib/rom/env.rb +11 -3
  24. data/lib/rom/global.rb +107 -0
  25. data/lib/rom/header.rb +122 -89
  26. data/lib/rom/header/attribute.rb +148 -0
  27. data/lib/rom/mapper.rb +46 -67
  28. data/lib/rom/mapper_builder.rb +20 -73
  29. data/lib/rom/mapper_builder/mapper_dsl.rb +114 -0
  30. data/lib/rom/mapper_builder/model_dsl.rb +29 -0
  31. data/lib/rom/mapper_registry.rb +21 -0
  32. data/lib/rom/model_builder.rb +11 -17
  33. data/lib/rom/processor.rb +28 -0
  34. data/lib/rom/processor/transproc.rb +105 -0
  35. data/lib/rom/reader.rb +81 -21
  36. data/lib/rom/reader_builder.rb +14 -4
  37. data/lib/rom/relation.rb +19 -5
  38. data/lib/rom/relation_builder.rb +20 -6
  39. data/lib/rom/repository.rb +0 -2
  40. data/lib/rom/setup.rb +156 -0
  41. data/lib/rom/{boot → setup}/base_relation_dsl.rb +4 -8
  42. data/lib/rom/setup/command_dsl.rb +46 -0
  43. data/lib/rom/setup/finalize.rb +125 -0
  44. data/lib/rom/setup/mapper_dsl.rb +19 -0
  45. data/lib/rom/{boot → setup}/relation_dsl.rb +1 -4
  46. data/lib/rom/setup/schema_dsl.rb +33 -0
  47. data/lib/rom/support/registry.rb +10 -6
  48. data/lib/rom/version.rb +1 -1
  49. data/rom.gemspec +3 -1
  50. data/spec/integration/adapters/extending_relations_spec.rb +0 -2
  51. data/spec/integration/commands/create_spec.rb +2 -9
  52. data/spec/integration/commands/delete_spec.rb +4 -5
  53. data/spec/integration/commands/error_handling_spec.rb +4 -3
  54. data/spec/integration/commands/update_spec.rb +3 -8
  55. data/spec/integration/mappers/deep_embedded_spec.rb +52 -0
  56. data/spec/integration/mappers/definition_dsl_spec.rb +0 -118
  57. data/spec/integration/mappers/embedded_spec.rb +82 -0
  58. data/spec/integration/mappers/group_spec.rb +170 -0
  59. data/spec/integration/mappers/prefixing_attributes_spec.rb +2 -2
  60. data/spec/integration/mappers/renaming_attributes_spec.rb +8 -6
  61. data/spec/integration/mappers/symbolizing_attributes_spec.rb +80 -0
  62. data/spec/integration/mappers/wrap_spec.rb +162 -0
  63. data/spec/integration/multi_repo_spec.rb +64 -0
  64. data/spec/integration/relations/reading_spec.rb +12 -8
  65. data/spec/integration/relations/registry_dsl_spec.rb +1 -3
  66. data/spec/integration/schema_spec.rb +10 -0
  67. data/spec/integration/setup_spec.rb +57 -6
  68. data/spec/spec_helper.rb +2 -1
  69. data/spec/unit/config_spec.rb +60 -0
  70. data/spec/unit/rom/adapter/memory/dataset_spec.rb +52 -0
  71. data/spec/unit/rom/adapter_spec.rb +31 -11
  72. data/spec/unit/rom/header_spec.rb +60 -16
  73. data/spec/unit/rom/mapper_builder_spec.rb +311 -0
  74. data/spec/unit/rom/mapper_registry_spec.rb +25 -0
  75. data/spec/unit/rom/mapper_spec.rb +4 -5
  76. data/spec/unit/rom/model_builder_spec.rb +15 -13
  77. data/spec/unit/rom/processor/transproc_spec.rb +331 -0
  78. data/spec/unit/rom/reader_spec.rb +73 -0
  79. data/spec/unit/rom/registry_spec.rb +38 -0
  80. data/spec/unit/rom/relation_spec.rb +0 -1
  81. data/spec/unit/rom/setup_spec.rb +55 -0
  82. data/spec/unit/rom_spec.rb +14 -0
  83. metadata +62 -22
  84. data/Gemfile.devtools +0 -71
  85. data/lib/rom/boot.rb +0 -197
  86. data/lib/rom/boot/command_dsl.rb +0 -48
  87. data/lib/rom/boot/dsl.rb +0 -37
  88. data/lib/rom/boot/mapper_dsl.rb +0 -23
  89. data/lib/rom/boot/schema_dsl.rb +0 -27
  90. data/lib/rom/ra.rb +0 -172
  91. data/lib/rom/ra/operation/group.rb +0 -47
  92. data/lib/rom/ra/operation/join.rb +0 -39
  93. data/lib/rom/ra/operation/wrap.rb +0 -45
  94. data/lib/rom/transformer.rb +0 -77
  95. data/spec/integration/ra/group_spec.rb +0 -46
  96. data/spec/integration/ra/join_spec.rb +0 -50
  97. data/spec/integration/ra/wrap_spec.rb +0 -37
  98. data/spec/unit/rom/ra/operation/group_spec.rb +0 -55
  99. data/spec/unit/rom/ra/operation/wrap_spec.rb +0 -29
  100. data/spec/unit/rom/transformer_spec.rb +0 -41
@@ -0,0 +1,29 @@
1
+ require 'rom/model_builder'
2
+
3
+ module ROM
4
+ class MapperBuilder
5
+ module ModelDSL
6
+ attr_reader :attributes, :builder, :klass
7
+
8
+ DEFAULT_TYPE = :poro
9
+
10
+ def model(options = nil)
11
+ if options.is_a?(Class)
12
+ @klass = options
13
+ elsif options
14
+ type = options.fetch(:type) { DEFAULT_TYPE }
15
+ @builder = ModelBuilder[type].new(options)
16
+ end
17
+
18
+ build_class unless options
19
+ end
20
+
21
+ private
22
+
23
+ def build_class
24
+ return klass if klass
25
+ return builder.call(attributes.map(&:first)) if builder
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module ROM
2
+ # @private
3
+ class MapperRegistry < Registry
4
+ # @api private
5
+ def []=(name, mapper)
6
+ elements[name] = mapper
7
+ end
8
+
9
+ # @api private
10
+ def by_path(path)
11
+ elements[paths(path).detect { |name| elements.key?(name) }]
12
+ end
13
+
14
+ private
15
+
16
+ # @api private
17
+ def paths(path)
18
+ path.split('.').map(&:to_sym).reverse
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,4 @@
1
1
  module ROM
2
-
3
2
  # @api private
4
3
  class ModelBuilder
5
4
  attr_reader :options, :const_name, :namespace, :klass
@@ -19,14 +18,15 @@ module ROM
19
18
  def initialize(options = {})
20
19
  @options = options
21
20
 
22
- if options[:name]
23
- split = options[:name].split('::')
21
+ name = options[:name]
22
+ if name
23
+ parts = name.split('::')
24
24
 
25
- @const_name = split.last
25
+ @const_name = parts.pop
26
26
 
27
27
  @namespace =
28
- if split.size > 1
29
- Inflecto.constantize((split-[const_name]).join('::'))
28
+ if parts.any?
29
+ Inflecto.constantize(parts.join('::'))
30
30
  else
31
31
  Object
32
32
  end
@@ -37,32 +37,26 @@ module ROM
37
37
  namespace.const_set(const_name, klass)
38
38
  end
39
39
 
40
- def call(header)
41
- define_class(header)
40
+ def call(attrs)
41
+ define_class(attrs)
42
42
  define_const if const_name
43
43
  @klass
44
44
  end
45
45
 
46
46
  class PORO < ModelBuilder
47
-
48
- def define_class(header)
47
+ def define_class(attrs)
49
48
  @klass = Class.new
50
49
 
51
- attributes = header.keys
52
-
53
- @klass.send(:attr_reader, *attributes)
50
+ @klass.send(:attr_reader, *attrs)
54
51
 
55
52
  @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
56
53
  def initialize(params)
57
- #{attributes.map { |name| "@#{name} = params[:#{name}]" }.join("\n")}
54
+ #{attrs.map { |name| "@#{name} = params[:#{name}]" }.join("\n")}
58
55
  end
59
56
  RUBY
60
57
 
61
58
  self
62
59
  end
63
-
64
60
  end
65
-
66
61
  end
67
-
68
62
  end
@@ -0,0 +1,28 @@
1
+ require 'rom/mapper'
2
+
3
+ module ROM
4
+ # Abstract processor class
5
+ #
6
+ # Every ROM processor should inherit from this class
7
+ #
8
+ # @public
9
+ class Processor
10
+ # Hook used to auto-register a processor class
11
+ #
12
+ # @api private
13
+ def self.inherited(processor)
14
+ Mapper.register_processor(processor)
15
+ end
16
+
17
+ # Required interface to be implemented by descendants
18
+ #
19
+ # @return [Processor]
20
+ #
21
+ # @abstract
22
+ #
23
+ # @api private
24
+ def self.build
25
+ raise NotImplementedError, "+build+ must be implemented"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,105 @@
1
+ require 'transproc/all'
2
+
3
+ require 'rom/processor'
4
+
5
+ module ROM
6
+ class Processor
7
+ class Transproc < Processor
8
+ include ::Transproc::Composer
9
+
10
+ attr_reader :header, :model, :mapping, :tuple_proc
11
+
12
+ EMPTY_FN = -> tuple { tuple }.freeze
13
+
14
+ def self.build(header)
15
+ new(header).to_transproc
16
+ end
17
+
18
+ def initialize(header)
19
+ @header = header
20
+ @model = header.model
21
+ @mapping = header.mapping
22
+ initialize_tuple_proc
23
+ end
24
+
25
+ def to_transproc
26
+ compose(EMPTY_FN) do |ops|
27
+ ops << header.groups.map { |attr| visit_group(attr, true) }
28
+ ops << t(:map_array!, tuple_proc) if tuple_proc
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def visit(attribute)
35
+ type = attribute.class.name.split('::').last.downcase
36
+ send("visit_#{type}", attribute)
37
+ end
38
+
39
+ def visit_attribute(attribute)
40
+ if attribute.typed?
41
+ t(:map_key!, attribute.name, t(:"to_#{attribute.type}"))
42
+ end
43
+ end
44
+
45
+ def visit_hash(attribute)
46
+ with_tuple_proc(attribute) do |tuple_proc|
47
+ t(:map_key!, attribute.name, tuple_proc)
48
+ end
49
+ end
50
+
51
+ def visit_array(attribute)
52
+ with_tuple_proc(attribute) do |tuple_proc|
53
+ t(:map_key!, attribute.name, t(:map_array!, tuple_proc))
54
+ end
55
+ end
56
+
57
+ def visit_wrap(attribute)
58
+ name = attribute.name
59
+ keys = attribute.tuple_keys
60
+
61
+ compose do |ops|
62
+ ops << t(:nest!, name, keys)
63
+ ops << visit_hash(attribute)
64
+ end
65
+ end
66
+
67
+ def visit_group(attribute, preprocess = false)
68
+ if preprocess
69
+ name = attribute.name
70
+ header = attribute.header
71
+ keys = attribute.tuple_keys
72
+
73
+ other = header.groups
74
+
75
+ compose do |ops|
76
+ ops << t(:group, name, keys)
77
+
78
+ ops << other.map { |attr|
79
+ t(:map_array!, t(:map_key!, name, visit_group(attr, true)))
80
+ }
81
+ end
82
+ else
83
+ visit_array(attribute)
84
+ end
85
+ end
86
+
87
+ def initialize_tuple_proc
88
+ @tuple_proc = compose do |ops|
89
+ ops << t(:map_hash!, mapping) if header.aliased?
90
+ ops << header.map { |attr| visit(attr) }
91
+ ops << t(-> tuple { model.new(tuple) }) if model
92
+ end
93
+ end
94
+
95
+ def with_tuple_proc(attribute)
96
+ tuple_proc = new(attribute.header).tuple_proc
97
+ yield(tuple_proc) if tuple_proc
98
+ end
99
+
100
+ def new(*args)
101
+ self.class.new(*args)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,27 +1,92 @@
1
1
  module ROM
2
-
3
2
  # Exposes mapped tuples via enumerable interface
4
3
  #
5
4
  # See example for each method
6
5
  #
7
6
  # @api public
8
7
  class Reader
8
+ MapperMissingError = Class.new(StandardError)
9
+
9
10
  include Enumerable
10
11
  include Equalizer.new(:path, :relation, :mapper)
11
12
 
12
- attr_reader :path, :relation, :header, :mappers, :mapper
13
+ # @return [String] access path used to read a relation
14
+ #
15
+ # @api private
16
+ attr_reader :path
17
+
18
+ # @return [Relation] relation used by the reader
19
+ #
20
+ # @api private
21
+ attr_reader :relation
22
+
23
+ # @return [MapperRegistry] registry of mappers used by the reader
24
+ #
25
+ # @api private
26
+ attr_reader :mappers
27
+
28
+ # @return [Mapper] mapper to read the relation
29
+ #
30
+ # @api private
31
+ attr_reader :mapper
32
+
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
37
+ #
38
+ # @param [Symbol] name of the root relation
39
+ # @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
+ #
43
+ # @return [Reader]
44
+ #
45
+ # @api private
46
+ 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
59
+
60
+ def self.to_s
61
+ name
62
+ 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
+ end
75
+
76
+ klass.new(name, relation, mappers)
77
+ end
13
78
 
14
79
  # @api private
15
80
  def initialize(path, relation, mappers = {})
16
81
  @path = path.to_s
17
82
  @relation = relation
18
83
  @mappers = mappers
84
+ @mapper = mappers.by_path(@path) || raise(MapperMissingError, path)
85
+ end
19
86
 
20
- names = @path.split('.')
21
-
22
- mapper_key = names.reverse.detect { |name| mappers.key?(name.to_sym) }
23
- @mapper = mappers.fetch(mapper_key.to_sym)
24
- @header = mapper.header
87
+ # @api private
88
+ def header
89
+ mapper.header
25
90
  end
26
91
 
27
92
  # Yields tuples mapped to objects
@@ -39,24 +104,19 @@ module ROM
39
104
  mapper.process(relation) { |tuple| yield(tuple) }
40
105
  end
41
106
 
42
- # @api private
43
- def respond_to_missing?(name, include_private = false)
44
- relation.respond_to?(name)
45
- end
46
-
47
107
  private
48
108
 
49
109
  # @api private
50
- def method_missing(name, *args, &block)
51
- new_relation = relation.public_send(name, *args, &block)
52
-
53
- splits = path.split('.')
54
- splits << name
55
- new_path = splits.join('.')
56
-
57
- self.class.new(new_path, new_relation, mappers)
110
+ def method_missing(name)
111
+ raise(
112
+ NoRelationError,
113
+ "undefined relation #{name.inspect} within #{path.inspect}"
114
+ )
58
115
  end
59
116
 
117
+ # @api private
118
+ def new_path(name)
119
+ path.dup << ".#{name}"
120
+ end
60
121
  end
61
-
62
122
  end
@@ -1,5 +1,6 @@
1
- module ROM
1
+ require 'rom/mapper_registry'
2
2
 
3
+ module ROM
3
4
  # @api private
4
5
  class ReaderBuilder
5
6
  DEFAULT_OPTIONS = { inherit_header: true }.freeze
@@ -21,10 +22,20 @@ module ROM
21
22
  builder.instance_exec(&block) if block
22
23
  mapper = builder.call
23
24
 
24
- mappers = options[:parent] ? readers.fetch(parent.name).mappers : {}
25
+ mappers =
26
+ if options[:parent]
27
+ readers.fetch(parent.name).mappers
28
+ else
29
+ MapperRegistry.new
30
+ end
25
31
 
26
32
  mappers[name] = mapper
27
- readers[name] = Reader.new(name, parent, mappers) unless options[:parent]
33
+
34
+ unless options[:parent]
35
+ readers[name] = Reader.build(
36
+ name, parent, mappers, parent.class.relation_methods
37
+ )
38
+ end
28
39
  end
29
40
  end
30
41
 
@@ -33,6 +44,5 @@ module ROM
33
44
  def with_options(options)
34
45
  yield(DEFAULT_OPTIONS.merge(options))
35
46
  end
36
-
37
47
  end
38
48
  end
@@ -1,11 +1,10 @@
1
1
  module ROM
2
-
3
2
  # Base relation class
4
3
  #
5
4
  # Relation is a proxy for the dataset object provided by the adapter, it
6
5
  # forwards every method to the dataset that's why "native" interface of the
7
6
  # underlying adapter is available in the relation. This interface, however, is
8
- # considered to private and should not be used outside of the relation instance.
7
+ # considered private and should not be used outside of the relation instance.
9
8
  #
10
9
  # ROM builds sub-classes of this class for every relation defined in the env
11
10
  # for easy inspection and extensibility - every adapter can provide extensions
@@ -21,11 +20,24 @@ module ROM
21
20
  include Charlatan.new(:dataset)
22
21
  include Equalizer.new(:header, :dataset)
23
22
 
23
+ class << self
24
+ # Relation methods that were defined inside setup.relation DSL
25
+ #
26
+ # @return [Array<Symbol>]
27
+ #
28
+ # @api private
29
+ attr_accessor :relation_methods
30
+ end
31
+
32
+ # @return [Array] relation base header
33
+ #
24
34
  # @api private
25
35
  attr_reader :header
26
36
 
37
+ # Hook to finalize a relation after its instance was created
38
+ #
27
39
  # @api private
28
- def self.finalize(env, relation)
40
+ def self.finalize(_env, _relation)
29
41
  # noop
30
42
  end
31
43
 
@@ -35,12 +47,14 @@ module ROM
35
47
  @header = header.dup.freeze
36
48
  end
37
49
 
50
+ # Yield dataset tuples
51
+ #
52
+ # @yield [Hash]
53
+ #
38
54
  # @api private
39
55
  def each(&block)
40
56
  return to_enum unless block
41
57
  dataset.each(&block)
42
58
  end
43
-
44
59
  end
45
-
46
60
  end