rom 0.4.2 → 0.5.0

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