rom 0.1.2 → 0.2.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 (139) hide show
  1. data/.rspec +2 -0
  2. data/.travis.yml +22 -0
  3. data/Changelog.md +16 -0
  4. data/Gemfile +13 -6
  5. data/Gemfile.devtools +71 -0
  6. data/Guardfile +19 -0
  7. data/LICENSE +1 -1
  8. data/README.md +52 -30
  9. data/Rakefile +3 -0
  10. data/config/devtools.yml +4 -0
  11. data/config/flay.yml +3 -0
  12. data/config/flog.yml +2 -0
  13. data/config/mutant.yml +5 -0
  14. data/config/reek.yml +103 -0
  15. data/config/rubocop.yml +62 -0
  16. data/config/yardstick.yml +2 -0
  17. data/lib/rom.rb +13 -5
  18. data/lib/rom/constants.rb +16 -0
  19. data/lib/rom/environment.rb +105 -0
  20. data/lib/rom/environment/builder.rb +71 -0
  21. data/lib/rom/mapper.rb +176 -0
  22. data/lib/rom/mapper/attribute.rb +108 -0
  23. data/lib/rom/mapper/builder.rb +58 -0
  24. data/lib/rom/mapper/builder/definition.rb +162 -0
  25. data/lib/rom/mapper/header.rb +103 -0
  26. data/lib/rom/mapper/loader_builder.rb +26 -0
  27. data/lib/rom/relation.rb +375 -0
  28. data/lib/rom/repository.rb +71 -0
  29. data/lib/rom/schema.rb +21 -0
  30. data/lib/rom/schema/builder.rb +59 -0
  31. data/lib/rom/schema/definition.rb +84 -0
  32. data/lib/rom/schema/definition/relation.rb +80 -0
  33. data/lib/rom/schema/definition/relation/base.rb +27 -0
  34. data/lib/rom/session.rb +111 -0
  35. data/lib/rom/session/environment.rb +67 -0
  36. data/lib/rom/session/identity_map.rb +43 -0
  37. data/lib/rom/session/mapper.rb +62 -0
  38. data/lib/rom/session/relation.rb +140 -0
  39. data/lib/rom/session/state.rb +59 -0
  40. data/lib/rom/session/state/created.rb +22 -0
  41. data/lib/rom/session/state/deleted.rb +25 -0
  42. data/lib/rom/session/state/persisted.rb +34 -0
  43. data/lib/rom/session/state/transient.rb +20 -0
  44. data/lib/rom/session/state/updated.rb +29 -0
  45. data/lib/rom/session/tracker.rb +62 -0
  46. data/lib/rom/support/axiom/adapter.rb +111 -0
  47. data/lib/rom/support/axiom/adapter/data_objects.rb +38 -0
  48. data/lib/rom/support/axiom/adapter/memory.rb +25 -0
  49. data/lib/rom/support/axiom/adapter/postgres.rb +19 -0
  50. data/lib/rom/support/axiom/adapter/sqlite3.rb +20 -0
  51. data/lib/version.rb +1 -1
  52. data/rom.gemspec +7 -3
  53. data/spec/integration/environment_setup_spec.rb +24 -0
  54. data/spec/integration/grouped_mappers_spec.rb +87 -0
  55. data/spec/integration/join_and_group_spec.rb +76 -0
  56. data/spec/integration/join_and_wrap_spec.rb +68 -0
  57. data/spec/integration/mapping_embedded_relations_spec.rb +73 -0
  58. data/spec/integration/mapping_relations_spec.rb +120 -0
  59. data/spec/integration/schema_definition_spec.rb +152 -0
  60. data/spec/integration/session_spec.rb +87 -0
  61. data/spec/integration/wrapped_mappers_spec.rb +73 -0
  62. data/spec/shared/unit/environment_context.rb +6 -0
  63. data/spec/shared/unit/loader.rb +11 -0
  64. data/spec/shared/unit/loader_identity.rb +13 -0
  65. data/spec/shared/unit/mapper_context.rb +11 -0
  66. data/spec/shared/unit/relation_context.rb +82 -0
  67. data/spec/shared/unit/session_environment_context.rb +11 -0
  68. data/spec/shared/unit/session_relation_context.rb +18 -0
  69. data/spec/spec_helper.rb +49 -0
  70. data/spec/support/helper.rb +34 -0
  71. data/spec/support/ice_nine_config.rb +10 -0
  72. data/spec/support/test_mapper.rb +110 -0
  73. data/spec/unit/rom/environment/builder/mapping_spec.rb +24 -0
  74. data/spec/unit/rom/environment/builder/schema_spec.rb +33 -0
  75. data/spec/unit/rom/environment/class_methods/setup_spec.rb +18 -0
  76. data/spec/unit/rom/environment/repository_spec.rb +21 -0
  77. data/spec/unit/rom/mapper/attribute/embedded_collection/to_ast_spec.rb +18 -0
  78. data/spec/unit/rom/mapper/attribute/embedded_value/to_ast_spec.rb +16 -0
  79. data/spec/unit/rom/mapper/attribute/rename_spec.rb +9 -0
  80. data/spec/unit/rom/mapper/attribute/to_ast_spec.rb +9 -0
  81. data/spec/unit/rom/mapper/builder/class_methods/call_spec.rb +61 -0
  82. data/spec/unit/rom/mapper/class_methods/build_spec.rb +55 -0
  83. data/spec/unit/rom/mapper/dump_spec.rb +11 -0
  84. data/spec/unit/rom/mapper/group_spec.rb +35 -0
  85. data/spec/unit/rom/mapper/header/each_spec.rb +26 -0
  86. data/spec/unit/rom/mapper/header/element_reader_spec.rb +21 -0
  87. data/spec/unit/rom/mapper/header/group_spec.rb +18 -0
  88. data/spec/unit/rom/mapper/header/join_spec.rb +14 -0
  89. data/spec/unit/rom/mapper/header/keys_spec.rb +29 -0
  90. data/spec/unit/rom/mapper/header/project_spec.rb +13 -0
  91. data/spec/unit/rom/mapper/header/rename_spec.rb +11 -0
  92. data/spec/unit/rom/mapper/header/to_ast_spec.rb +11 -0
  93. data/spec/unit/rom/mapper/header/wrap_spec.rb +18 -0
  94. data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +11 -0
  95. data/spec/unit/rom/mapper/identity_spec.rb +11 -0
  96. data/spec/unit/rom/mapper/join_spec.rb +15 -0
  97. data/spec/unit/rom/mapper/load_spec.rb +11 -0
  98. data/spec/unit/rom/mapper/new_object_spec.rb +14 -0
  99. data/spec/unit/rom/mapper/project_spec.rb +11 -0
  100. data/spec/unit/rom/mapper/rename_spec.rb +16 -0
  101. data/spec/unit/rom/mapper/wrap_spec.rb +35 -0
  102. data/spec/unit/rom/relation/delete_spec.rb +15 -0
  103. data/spec/unit/rom/relation/drop_spec.rb +11 -0
  104. data/spec/unit/rom/relation/each_spec.rb +23 -0
  105. data/spec/unit/rom/relation/first_spec.rb +19 -0
  106. data/spec/unit/rom/relation/group_spec.rb +29 -0
  107. data/spec/unit/rom/relation/inject_mapper_spec.rb +17 -0
  108. data/spec/unit/rom/relation/insert_spec.rb +13 -0
  109. data/spec/unit/rom/relation/last_spec.rb +19 -0
  110. data/spec/unit/rom/relation/one_spec.rb +49 -0
  111. data/spec/unit/rom/relation/rename_spec.rb +21 -0
  112. data/spec/unit/rom/relation/replace_spec.rb +13 -0
  113. data/spec/unit/rom/relation/restrict_spec.rb +25 -0
  114. data/spec/unit/rom/relation/sort_by_spec.rb +25 -0
  115. data/spec/unit/rom/relation/take_spec.rb +11 -0
  116. data/spec/unit/rom/relation/to_a_spec.rb +20 -0
  117. data/spec/unit/rom/relation/update_spec.rb +25 -0
  118. data/spec/unit/rom/relation/wrap_spec.rb +29 -0
  119. data/spec/unit/rom/repository/class_methods/build_spec.rb +27 -0
  120. data/spec/unit/rom/repository/element_reader_spec.rb +21 -0
  121. data/spec/unit/rom/repository/element_writer_spec.rb +18 -0
  122. data/spec/unit/rom/schema/builder/class_methods/build_spec.rb +103 -0
  123. data/spec/unit/rom/schema/element_reader_spec.rb +15 -0
  124. data/spec/unit/rom/session/class_methods/start_spec.rb +23 -0
  125. data/spec/unit/rom/session/clean_predicate_spec.rb +21 -0
  126. data/spec/unit/rom/session/environment/element_reader_spec.rb +13 -0
  127. data/spec/unit/rom/session/flush_spec.rb +58 -0
  128. data/spec/unit/rom/session/mapper/load_spec.rb +47 -0
  129. data/spec/unit/rom/session/relation/delete_spec.rb +28 -0
  130. data/spec/unit/rom/session/relation/dirty_predicate_spec.rb +35 -0
  131. data/spec/unit/rom/session/relation/identity_spec.rb +11 -0
  132. data/spec/unit/rom/session/relation/new_spec.rb +50 -0
  133. data/spec/unit/rom/session/relation/save_spec.rb +50 -0
  134. data/spec/unit/rom/session/relation/state_spec.rb +23 -0
  135. data/spec/unit/rom/session/relation/track_spec.rb +23 -0
  136. data/spec/unit/rom/session/relation/tracking_predicate_spec.rb +23 -0
  137. data/spec/unit/rom/session/relation/update_attributes_spec.rb +45 -0
  138. data/spec/unit/rom/session/state_spec.rb +79 -0
  139. metadata +212 -11
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 100
data/lib/rom.rb CHANGED
@@ -1,6 +1,14 @@
1
- module ROM
2
- end
1
+ # encoding: utf-8
3
2
 
4
- require 'rom-relation'
5
- require 'rom-mapper'
6
- require 'rom-session'
3
+ require 'adamantium'
4
+ require 'concord'
5
+ require 'equalizer'
6
+ require 'charlatan'
7
+ require 'morpher'
8
+ require 'axiom'
9
+ require 'axiom-optimizer'
10
+
11
+ require 'rom/environment'
12
+ require 'rom/relation'
13
+ require 'rom/mapper'
14
+ require 'rom/session'
@@ -0,0 +1,16 @@
1
+ module ROM
2
+ # Raised when the returned tuples are unexpectedly empty
3
+ NoTuplesError = Class.new(RuntimeError)
4
+
5
+ # Raised when the returned tuples are unexpectedly too many
6
+ ManyTuplesError = Class.new(RuntimeError)
7
+
8
+ # Represent an undefined argument
9
+ Undefined = Class.new.freeze
10
+
11
+ # An empty frozen Hash useful for parameter default values
12
+ EMPTY_HASH = {}.freeze
13
+
14
+ # An empty frozen Array useful for parameter default values
15
+ EMPTY_ARRAY = [].freeze
16
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rom/environment/builder'
4
+
5
+ module ROM
6
+
7
+ # The environment configures repositories and loads schema with relations
8
+ #
9
+ class Environment
10
+ # @api private
11
+ attr_reader :repositories, :relations
12
+
13
+ # Return schema registry
14
+ #
15
+ # @return [Schema]
16
+ #
17
+ # @api public
18
+ attr_reader :schema
19
+
20
+ # Return mapper registry
21
+ #
22
+ # @return [Hash]
23
+ #
24
+ # @api public
25
+ attr_reader :mappers
26
+
27
+ # @api private
28
+ def initialize(repositories, schema, relations, mappers)
29
+ @repositories = repositories
30
+ @schema = schema
31
+ @relations = relations
32
+ @mappers = mappers
33
+ end
34
+
35
+ # Setup ROM environment
36
+ #
37
+ # @example
38
+ #
39
+ # env = ROM::Environment.setup(test: 'memory://test') do
40
+ # schema do
41
+ # base_relation(:users) do
42
+ # repository :test
43
+ #
44
+ # attribute :id, Integer
45
+ # attribute :name, String
46
+ #
47
+ # key :id
48
+ # end
49
+ # end
50
+ #
51
+ # mapping do
52
+ # relation(:users) do
53
+ # model User
54
+ #
55
+ # map :id, :name
56
+ # end
57
+ # end
58
+ #
59
+ # end
60
+ #
61
+ # @param [Environment, Hash<#to_sym, String>] config
62
+ # an environment or a hash of adapter uri strings,
63
+ # keyed by repository name
64
+ #
65
+ # @return [Environment::Builder]
66
+ #
67
+ # @api public
68
+ def self.setup(config, &block)
69
+ builder = Builder.call(config)
70
+
71
+ if block
72
+ builder.instance_eval(&block)
73
+ builder.finalize
74
+ else
75
+ builder
76
+ end
77
+ end
78
+
79
+ # Return registered relation
80
+ #
81
+ # @example
82
+ #
83
+ # env[:users]
84
+ #
85
+ # @param [Symbol] relation name
86
+ #
87
+ # @return [Relation]
88
+ #
89
+ # @api public
90
+ def [](name)
91
+ relations[name]
92
+ end
93
+
94
+ # The repository with the given +name+
95
+ #
96
+ # @return [Repository]
97
+ #
98
+ # @api public
99
+ def repository(name)
100
+ repositories[name]
101
+ end
102
+
103
+ end # Environment
104
+
105
+ end # ROM
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ require 'addressable/uri'
4
+
5
+ require 'rom/environment'
6
+ require 'rom/repository'
7
+ require 'rom/schema/builder'
8
+ require 'rom/mapper/builder'
9
+
10
+ module ROM
11
+
12
+ # The environment configures repositories and loads schema with relations
13
+ #
14
+ class Environment
15
+
16
+ # Environment builder DSL
17
+ #
18
+ class Builder
19
+ attr_reader :repositories, :relations, :mappers
20
+
21
+ # @api private
22
+ def self.call(config)
23
+ repositories = config.each_with_object({}) { |(name, uri), hash|
24
+ hash[name.to_sym] = Repository.build(name, Addressable::URI.parse(uri))
25
+ }
26
+
27
+ new(repositories)
28
+ end
29
+
30
+ # @api private
31
+ def initialize(repositories)
32
+ @repositories = repositories
33
+ @relations = {}
34
+ @schema = Schema::Builder.build(repositories)
35
+ @mappers = Mapper::Builder.new(schema)
36
+ end
37
+
38
+ # @api private
39
+ def schema(&block)
40
+ @schema.call(&block) if block
41
+ @schema
42
+ end
43
+
44
+ # @api private
45
+ def mapping(&block)
46
+ mappers.call(&block)
47
+ end
48
+
49
+ # @api private
50
+ def [](name)
51
+ relations[name]
52
+ end
53
+
54
+ # @api private
55
+ def []=(name, relation)
56
+ relations[name] = relation
57
+ end
58
+
59
+ # @api private
60
+ def finalize
61
+ mappers.each do |name, mapper|
62
+ relations[name] = Relation.new(schema[name], mapper)
63
+ end
64
+
65
+ Environment.new(repositories, schema.finalize, relations, mappers.finalize)
66
+ end
67
+
68
+ end # Builder
69
+
70
+ end # Environment
71
+ end # ROM
@@ -0,0 +1,176 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ostruct'
4
+
5
+ require 'rom/constants'
6
+ require 'rom/mapper/header'
7
+ require 'rom/mapper/loader_builder'
8
+
9
+ module ROM
10
+
11
+ # Mappers load tuples into objects and dump objects back into tuples
12
+ #
13
+ class Mapper
14
+ include Equalizer.new(:header, :options)
15
+
16
+ DEFAULT_LOADER = :load_instance_variables
17
+
18
+ attr_reader :header, :loader, :dumper, :model, :type, :options
19
+
20
+ # Build a mapper
21
+ #
22
+ # @example
23
+ #
24
+ # User = Class.new { attr_reader :id, :name }
25
+ #
26
+ # mapper = ROM::Mapper.build([[:id, from: :user_id], [:name, from: :user_name]], model: User)
27
+ #
28
+ # user = mapper.load(user_id: 1, user_name: 'Jane')
29
+ # # => #<User:0x007fee3b8bf2c8 @id=1, @name="Jane">
30
+ #
31
+ # tuple = mapper.dump(user)
32
+ # # => [1, "Jane"]
33
+ #
34
+ # @return [Mapper]
35
+ #
36
+ # @api public
37
+ def self.build(attributes, options = EMPTY_HASH)
38
+ defaults = { type: DEFAULT_LOADER, model: OpenStruct }.update(options)
39
+
40
+ header = Header.build(attributes)
41
+ loader = LoaderBuilder.call(header, defaults[:model], defaults[:type])
42
+
43
+ new(header, loader, defaults)
44
+ end
45
+
46
+ # @api private
47
+ def initialize(header, loader, options)
48
+ @header = header
49
+ @loader = loader
50
+ @dumper = loader.inverse
51
+ @model = options.fetch(:model)
52
+ @type = options.fetch(:type)
53
+ @options = options
54
+ end
55
+
56
+ # Retrieve identity from the given object
57
+ #
58
+ # @example
59
+ #
60
+ # mapper.identity(user) # => [1]
61
+ #
62
+ # @param [Object]
63
+ #
64
+ # @return [Array]
65
+ #
66
+ # @api public
67
+ def identity(object)
68
+ header.keys.map { |key| object.send(key.name) }
69
+ end
70
+
71
+ # Return identity from the given tuple
72
+ #
73
+ # @example
74
+ #
75
+ # mapper.identity_from_tuple({id: 1}) # => [1]
76
+ #
77
+ # @param [Axiom::Tuple,Hash]
78
+ #
79
+ # @return [Array]
80
+ #
81
+ # @api public
82
+ def identity_from_tuple(tuple)
83
+ header.keys.map { |key| tuple[key.name] }
84
+ end
85
+
86
+ # Build a new model instance
87
+ #
88
+ # @example
89
+ #
90
+ # mapper = Mapper.build(header, User)
91
+ # mapper.new_object(id: 1, name: 'Jane') # => #<User @id=1 @name="Jane">
92
+ #
93
+ # @api public
94
+ def new_object(*args, &block)
95
+ model.new(*args, &block)
96
+ end
97
+
98
+ # Load an object instance from the tuple
99
+ #
100
+ # @api public
101
+ def load(tuple)
102
+ loader.call(tuple)
103
+ end
104
+
105
+ # Dump an object into a tuple
106
+ #
107
+ # @api public
108
+ #
109
+ # TODO: it's not clear how a tuple should look like for grouped/wrapped
110
+ # relation. the current implementation is temporary
111
+ def dump(object)
112
+ ary = dumper.call(object)
113
+
114
+ ary.each_with_object([]) do |(name, value), tuple|
115
+ attribute = header.detect { |attr| attr.tuple_key == name }
116
+
117
+ if attribute.respond_to?(:header)
118
+ names = attribute.header.attribute_names
119
+
120
+ if value.is_a?(Hash)
121
+ tuple << value.values_at(*names)
122
+ elsif value.is_a?(Array)
123
+ tuple << value.map { |v| v.values_at(*names) }
124
+ else
125
+ raise NotImplementedError
126
+ end
127
+ else
128
+ tuple << value
129
+ end
130
+ end
131
+ end
132
+
133
+ # TODO: this should map the wrapping hash into {Symbol => Mapper::Header}
134
+ # otherwise header is coupled to mapper
135
+ #
136
+ # @api public
137
+ def wrap(other)
138
+ new(header.wrap(other))
139
+ end
140
+
141
+ # TODO: this should map the grouping hash into {Symbol => Mapper::Header}
142
+ # otherwise header is coupled to mapper
143
+ #
144
+ # @api public
145
+ def group(other)
146
+ new(header.group(other))
147
+ end
148
+
149
+ # @api public
150
+ def join(other)
151
+ new(header.join(other.header))
152
+ end
153
+
154
+ # @api public
155
+ def project(names)
156
+ new(header.project(names))
157
+ end
158
+
159
+ # @api public
160
+ def rename(names)
161
+ new(header.rename(names))
162
+ end
163
+
164
+ # @api private
165
+ def attribute(type, name)
166
+ type.build(name, type: model, header: header, node: loader.node)
167
+ end
168
+
169
+ # @api private
170
+ def new(new_header)
171
+ self.class.build(new_header, options)
172
+ end
173
+
174
+ end # Mapper
175
+
176
+ end # ROM
@@ -0,0 +1,108 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Mapper
5
+
6
+ # Represents a mapping attribute
7
+ #
8
+ # @private
9
+ class Attribute
10
+ include Adamantium, Concord::Public.new(:name, :options), Morpher::NodeHelpers
11
+
12
+ class EmbeddedValue < Attribute
13
+
14
+ # @api private
15
+ def to_ast
16
+ s(:key_transform, name, name, node)
17
+ end
18
+ memoize :to_ast
19
+
20
+ # @api private
21
+ def header
22
+ options.fetch(:header)
23
+ end
24
+ memoize :header
25
+
26
+ private
27
+
28
+ # @api private
29
+ def node
30
+ options.fetch(:node)
31
+ end
32
+ memoize :node
33
+ end
34
+
35
+ class EmbeddedCollection < Attribute
36
+
37
+ # @api private
38
+ def to_ast
39
+ s(:key_transform, name, name, s(:map, node))
40
+ end
41
+ memoize :to_ast
42
+
43
+ # @api private
44
+ def header
45
+ options.fetch(:header)
46
+ end
47
+ memoize :header
48
+
49
+ private
50
+
51
+ # @api private
52
+ def node
53
+ options.fetch(:node)
54
+ end
55
+ memoize :node
56
+ end
57
+
58
+ # @api private
59
+ def self.build(*args)
60
+ input = args.first
61
+
62
+ if input.kind_of?(self)
63
+ input
64
+ else
65
+ name, options = args
66
+ new(name, options || {})
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def to_ast
72
+ s(:block, s(:key_fetch, tuple_key), s(:key_dump, name))
73
+ end
74
+ memoize :to_ast
75
+
76
+ # @api private
77
+ def key?
78
+ options.fetch(:key, false)
79
+ end
80
+ memoize :key?
81
+
82
+ # @api private
83
+ def mapping
84
+ { tuple_key => name }
85
+ end
86
+ memoize :mapping
87
+
88
+ # @api private
89
+ def tuple_key
90
+ options[:from] || name
91
+ end
92
+ memoize :tuple_key
93
+
94
+ # @api private
95
+ def type
96
+ options[:type] || Object
97
+ end
98
+ memoize :type
99
+
100
+ # @api private
101
+ def rename(new_name)
102
+ self.class.new(new_name, options)
103
+ end
104
+
105
+ end # Attribute
106
+
107
+ end # Mapper
108
+ end # ROM