rom-repository 0.2.0 → 0.3.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -5
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +21 -5
  6. data/README.md +6 -110
  7. data/lib/rom/repository/changeset/create.rb +26 -0
  8. data/lib/rom/repository/changeset/pipe.rb +40 -0
  9. data/lib/rom/repository/changeset/update.rb +82 -0
  10. data/lib/rom/repository/changeset.rb +99 -0
  11. data/lib/rom/repository/class_interface.rb +142 -0
  12. data/lib/rom/repository/command_compiler.rb +214 -0
  13. data/lib/rom/repository/command_proxy.rb +22 -0
  14. data/lib/rom/repository/header_builder.rb +13 -16
  15. data/lib/rom/repository/mapper_builder.rb +7 -14
  16. data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
  17. data/lib/rom/repository/relation_proxy.rb +225 -0
  18. data/lib/rom/repository/root.rb +110 -0
  19. data/lib/rom/repository/struct_attributes.rb +46 -0
  20. data/lib/rom/repository/struct_builder.rb +31 -14
  21. data/lib/rom/repository/version.rb +1 -1
  22. data/lib/rom/repository.rb +192 -31
  23. data/lib/rom/struct.rb +13 -8
  24. data/rom-repository.gemspec +9 -10
  25. data/spec/integration/changeset_spec.rb +86 -0
  26. data/spec/integration/command_macros_spec.rb +175 -0
  27. data/spec/integration/command_spec.rb +224 -0
  28. data/spec/integration/multi_adapter_spec.rb +3 -3
  29. data/spec/integration/repository_spec.rb +97 -2
  30. data/spec/integration/root_repository_spec.rb +88 -0
  31. data/spec/shared/database.rb +47 -3
  32. data/spec/shared/mappers.rb +35 -0
  33. data/spec/shared/models.rb +41 -0
  34. data/spec/shared/plugins.rb +66 -0
  35. data/spec/shared/relations.rb +76 -0
  36. data/spec/shared/repo.rb +38 -17
  37. data/spec/shared/seeds.rb +19 -0
  38. data/spec/spec_helper.rb +4 -1
  39. data/spec/support/mapper_registry.rb +1 -3
  40. data/spec/unit/changeset_spec.rb +58 -0
  41. data/spec/unit/header_builder_spec.rb +34 -35
  42. data/spec/unit/relation_proxy_spec.rb +170 -0
  43. data/spec/unit/sql/relation_spec.rb +5 -5
  44. data/spec/unit/struct_builder_spec.rb +7 -4
  45. data/spec/unit/struct_spec.rb +22 -0
  46. metadata +38 -41
  47. data/lib/rom/plugins/relation/key_inference.rb +0 -31
  48. data/lib/rom/repository/loading_proxy/combine.rb +0 -158
  49. data/lib/rom/repository/loading_proxy.rb +0 -182
  50. data/spec/unit/loading_proxy_spec.rb +0 -147
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ddf58d5430fe24bfd9afa8622196cfab93cfa51
4
- data.tar.gz: a6c276d87c6c791b32b7337ce40c0c7f0be968ba
3
+ metadata.gz: bac31b2dc1dafd64ec8a3852d1cb9e595c49ded5
4
+ data.tar.gz: 9110fc0e2f6d1b742ed89c4f7afb5c9b61ad0cdc
5
5
  SHA512:
6
- metadata.gz: b13a6f310a5e5b6ec18889327580a37de2d3ed105a68a26ce890b7114d4180f7f76e8e025b398d659a75bfcba1cb1807673133478dc8bbae3c9b217296d0f890
7
- data.tar.gz: cd06c75ae5edcbab75c366cdb045aeda5645fe8c0f7bc62639ec270d427f1918f282061293d74799ffcdb3c52df3ecc2d6778067df3075a698e1c89fa0ff686e
6
+ metadata.gz: 77b81f8ab9040eeaa5059dee76046f2330ebaddc75c1dad792e1b7e911d42bd4056d364d5b59194208c55d366d3d3e3358376292abfa2580f4e96a86b3ab1e83
7
+ data.tar.gz: c6b9d0809edd8b5b8742ff9de4a1761c287f502706416bf698453a1153cea2f122972d0dcbfdc6d06ead0acc55100b9a1f4d230fd13d24bb536e0969b301d4ac
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  Gemfile.lock
2
+ log/*.log
data/.travis.yml CHANGED
@@ -6,13 +6,11 @@ before_script:
6
6
  - psql -c 'create database rom_repository' -U postgres
7
7
  script: "bundle exec rake ci"
8
8
  rvm:
9
- - 2.0
10
9
  - 2.1
11
10
  - 2.2
12
- - 2.3.0
11
+ - 2.3.1
13
12
  - rbx-2
14
- - jruby-9000
15
- - jruby-head
13
+ - jruby-9.0.5.0
16
14
  - ruby-head
17
15
  env:
18
16
  global:
@@ -22,7 +20,9 @@ matrix:
22
20
  allow_failures:
23
21
  - rvm: ruby-head
24
22
  - rvm: jruby-head
25
- - rvm: rbx-2
23
+ include:
24
+ - rvm: jruby-head
25
+ before_install: gem install bundler --no-ri --no-rdoc
26
26
  notifications:
27
27
  webhooks:
28
28
  urls:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # v0.3.0 2016-07-27
2
+
3
+ ### Added
4
+
5
+ * `Repository#command` for inferring commands automatically from relations (solnic)
6
+ * `Repository.commands` macro which generates command methods (solnic)
7
+ * `Repository[rel_name]` for setting up a repository with a root relation (solnic)
8
+ * `Repository#aggregate` as a shortcut for composing relation graphs from root (solnic)
9
+ * `Repository#changeset` API for building specialized objects for handling changes in relations (solnic)
10
+
11
+ ### Fixed
12
+
13
+ * Auto-mapping includes default custom mapper for a given relation (AMHOL)
14
+ * When custom mapper is set, default struct mapper won't be used (AMHOL)
15
+
16
+ ### Changed
17
+
18
+ * `Relation#combine` supports passing name of configured associations for automatic relation composition (solnic)
19
+ * `Repository` constructor simply expects a rom container, which makes it work with DI libs like `dry-auto_inject` (solnic)
20
+ * Depends on `rom 2.0.0` now (solnic)
21
+ * Replace anima with `ROM::Repository::StructAttributes` (flash-gordon)
22
+
23
+ [Compare v0.2.0...v0.3.0](https://github.com/rom-rb/rom-repository/compare/v0.2.0...v0.3.0)
24
+
1
25
  # v0.2.0 2016-01-06
2
26
 
3
27
  ### Added
data/Gemfile CHANGED
@@ -4,14 +4,30 @@ gemspec
4
4
 
5
5
  gem 'inflecto'
6
6
 
7
+ group :development, :test do
8
+ gem 'rom-sql', '~> 0.8'
9
+ end
10
+
11
+ group :development do
12
+ gem 'dry-equalizer', '~> 0.2'
13
+ gem 'sqlite3', platforms: [:mri, :rbx]
14
+ gem 'jdbc-sqlite3', platforms: :jruby
15
+ end
16
+
7
17
  group :test do
8
- gem 'anima', '~> 0.2.0'
9
- gem 'rom-sql', '~> 0.7.0'
10
18
  gem 'rspec'
11
19
  gem 'byebug', platforms: :mri
12
20
  gem 'pg', platforms: [:mri, :rbx]
13
- gem 'pg_jruby', platforms: :jruby
14
- gem "codeclimate-test-reporter", require: nil
21
+ gem 'jdbc-postgres', platforms: :jruby
22
+ gem 'codeclimate-test-reporter', require: nil
15
23
  end
16
24
 
17
- gem 'benchmark-ips'
25
+ group :benchmarks do
26
+ gem 'hotch', platforms: :mri
27
+ gem 'benchmark-ips'
28
+ gem 'activerecord', '~> 4.2'
29
+ end
30
+
31
+ group :tools do
32
+ gem 'pry'
33
+ end
data/README.md CHANGED
@@ -4,125 +4,21 @@
4
4
  [codeclimate]: https://codeclimate.com/github/rom-rb/rom-repository
5
5
  [inchpages]: http://inch-ci.org/github/rom-rb/rom-repository
6
6
 
7
- # ROM::Repository
7
+ # rom-repository
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/rom-repository.svg)][gem]
10
10
  [![Build Status](https://travis-ci.org/rom-rb/rom-repository.svg?branch=master)][travis]
11
- [![Dependency Status](https://gemnasium.com/rom-rb/rom-repository.png)][gemnasium]
11
+ [![Dependency Status](https://gemnasium.com/rom-rb/rom-repository.svg)][gemnasium]
12
12
  [![Code Climate](https://codeclimate.com/github/rom-rb/rom-repository/badges/gpa.svg)][codeclimate]
13
13
  [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
14
14
  [![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][inchpages]
15
15
 
16
- Repository for [ROM](https://github.com/rom-rb/rom) with auto-mapping and relation
17
- extensions.
16
+ Repository for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping and commands.
18
17
 
19
- ## Conventions & Definitions
18
+ Resources:
20
19
 
21
- Repositories in ROM are simple objects that allow you to compose rich relation
22
- views specific to your application layer. Every repository can access multiple
23
- relations that you define and use them to build more complex views.
24
-
25
- Repository relations are enhanced with a couple of extra features on top of ROM
26
- relations:
27
-
28
- - Every relation has an auto-generated mapper which turns raw data into simple, immutable structs
29
- - `Relation#combine` can accept a simple hash defining what other relations should be joined
30
- - `Relation#combine_parents` automatically joins parents using eager-loading
31
- - `Relation#combine_children` automatically joins children using eager-loading
32
- - `Relation#wrap_parent` automatically joins a parent using inner join
33
-
34
- ### Relation Views
35
-
36
- A relation view is a result of some query which returns results specific to your
37
- application. You can define them using a simple DSL where you specify a name, what
38
- attributes resulting tuples will have and of course the query itself:
39
-
40
- ``` ruby
41
- class Users < ROM::Relation[:sql]
42
- view(:by_id, [:id, :name]) do |id|
43
- where(id: id).select(:id, :name)
44
- end
45
-
46
- view(:listing, [:id, :name, :email, :created_at]) do
47
- select(:id, :name, :email, :created_at).order(:name)
48
- end
49
- end
50
- ```
51
-
52
- This way we can explicitly define all our relation view that our application will
53
- depend on. It encapsulates access to application-specific data structures and allows
54
- you to easily test individual views in isolation.
55
-
56
- Thanks to explicit definition of attributes mappers are derived automatically.
57
-
58
- ### Auto-combine & Auto-wrap
59
-
60
- Repository relations support automatic `combine` and `wrap` by using a simple
61
- convention that every relation defines `for_combine(keys, other)` and `for_wrap(keys, other)`.
62
-
63
- You can override the default behavior for combine by defining `for_other_rel_name`
64
- in example, if you combine tasks with users you can define `Tasks#for_users` and
65
- this will be used instead of the generic `for_combine`.
66
-
67
- ### Mapping & Structs
68
-
69
- Currently repositories map to `ROM::Struct` by default. In the near future this
70
- will be configurable.
71
-
72
- ROM structs are simple and don't expose an interface to mutate them; however, they
73
- are not being frozen (at least not yet, we could add a feature for freezing them).
74
-
75
- They are coercible to `Hash` so it should be possible to map them further in some
76
- special cases using ROM mappers.
77
-
78
- ``` ruby
79
- class Users < ROM::Relation[:sql]
80
- view(:by_id, [:id, :name]) do |id|
81
- where(id: id).select(:id, :name)
82
- end
83
-
84
- view(:listing, [:id, :name, :email, :created_at]) do
85
- select(:id, :name, :email, :created_at).order(:name)
86
- end
87
- end
88
-
89
- class UserRepository < ROM::Repository::Base
90
- relations :users, :tasks
91
-
92
- def by_id(id)
93
- users.by_id(id)
94
- end
95
-
96
- def with_tasks(id)
97
- users.by_id(id).combine_children(many: tasks)
98
- end
99
- end
100
-
101
- rom = ROM.finalize.env
102
-
103
- user_repo = UserRepository.new(rom)
104
-
105
- puts user_repo.by_id(1).to_a.inspect
106
- # [#<ROM::Struct[User] id=1 name="Jane">]
107
-
108
- puts user_repo.with_tasks.to_a.inspect
109
- # [#<ROM::Struct[User] id=1 name="Jane" tasks=[#<ROM::Struct[Task] id=2 user_id=1 title="Jane Task">]>, #<ROM::Struct[User] id=2 name="Joe" tasks=[#<ROM::Struct[Task] id=1 user_id=2 title="Joe Task">]>]
110
- ```
111
-
112
- ### Using Custom Model Types
113
-
114
- To use a custom model type you simply use the standard `Relation#as` inteface
115
- but you can pass a constant:
116
-
117
- ``` ruby
118
- class UserRepository < ROM::Repository::Base
119
- relations :users, :tasks
120
-
121
- def by_id(id)
122
- users.by_id(id).as(User)
123
- end
124
- end
125
- ```
20
+ * [User documentation](http://rom-rb.org/learn/repositories)
21
+ * [API documentation](http://rubydoc.info/gems/rom-repository)
126
22
 
127
23
  ## License
128
24
 
@@ -0,0 +1,26 @@
1
+ module ROM
2
+ class Changeset
3
+ # Changeset specialization for create commands
4
+ #
5
+ # @api public
6
+ class Create < Changeset
7
+ # Return false
8
+ #
9
+ # @return [FalseClass]
10
+ #
11
+ # @api public
12
+ def update?
13
+ false
14
+ end
15
+
16
+ # Return true
17
+ #
18
+ # @return [TrueClass]
19
+ #
20
+ # @api public
21
+ def create?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'transproc/registry'
2
+
3
+ module ROM
4
+ class Changeset
5
+ class Pipe
6
+ extend Transproc::Registry
7
+
8
+ attr_reader :processor
9
+
10
+ def self.add_timestamps(data)
11
+ now = Time.now
12
+ data.merge(created_at: now, updated_at: now)
13
+ end
14
+
15
+ def self.touch(data)
16
+ data.merge(updated_at: Time.now)
17
+ end
18
+
19
+ def initialize(processor = nil)
20
+ @processor = processor
21
+ end
22
+
23
+ def >>(other)
24
+ if processor
25
+ self.class.new(processor >> other)
26
+ else
27
+ self.class.new(other)
28
+ end
29
+ end
30
+
31
+ def call(data)
32
+ if processor
33
+ processor.call(data)
34
+ else
35
+ data
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,82 @@
1
+ module ROM
2
+ class Changeset
3
+ # Changeset specialization for update commands
4
+ #
5
+ # @api public
6
+ class Update < Changeset
7
+ # @!attribute [r] primary_key
8
+ # @return [Symbol] The name of the relation's primary key attribute
9
+ option :primary_key, reader: true
10
+
11
+ # Return true
12
+ #
13
+ # @return [TrueClass]
14
+ #
15
+ # @api public
16
+ def update?
17
+ true
18
+ end
19
+
20
+ # Return false
21
+ #
22
+ # @return [FalseClass]
23
+ #
24
+ # @api public
25
+ def create?
26
+ false
27
+ end
28
+
29
+ # Return original tuple that this changeset may update
30
+ #
31
+ # @return [Hash]
32
+ #
33
+ # @api public
34
+ def original
35
+ @original ||= relation.fetch(primary_key)
36
+ end
37
+
38
+ # Return diff hash sent through the pipe
39
+ #
40
+ # @return [Hash]
41
+ #
42
+ # @api public
43
+ def to_h
44
+ pipe.call(diff)
45
+ end
46
+ alias_method :to_hash, :to_h
47
+
48
+ # Return true if there's a diff between original and changeset data
49
+ #
50
+ # @return [TrueClass, FalseClass]
51
+ #
52
+ # @api public
53
+ def diff?
54
+ ! diff.empty?
55
+ end
56
+
57
+ # Return if there's no diff between the original and changeset data
58
+ #
59
+ # @return [TrueClass, FalseClass]
60
+ #
61
+ # @api public
62
+ def clean?
63
+ diff.empty?
64
+ end
65
+
66
+ # Calculate the diff between the original and changeset data
67
+ #
68
+ # @return [Hash[
69
+ #
70
+ # @api public
71
+ def diff
72
+ @diff ||=
73
+ begin
74
+ new_tuple = data.to_a
75
+ ori_tuple = original.to_a
76
+
77
+ Hash[new_tuple - (new_tuple & ori_tuple)]
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,99 @@
1
+ require 'rom/support/constants'
2
+ require 'rom/support/options'
3
+
4
+ require 'rom/repository/changeset/pipe'
5
+
6
+ module ROM
7
+ class Changeset
8
+ include Options
9
+
10
+ # @!attribute [r] pipe
11
+ # @return [Changeset::Pipe] data transformation pipe
12
+ option :pipe, reader: true, accept: [Proc, Pipe], default: -> changeset {
13
+ changeset.class.default_pipe
14
+ }
15
+
16
+ # @!attribute [r] relation
17
+ # @return [Relation] The changeset relation
18
+ attr_reader :relation
19
+
20
+ # @!attribute [r] data
21
+ # @return [Hash] The relation data
22
+ attr_reader :data
23
+
24
+ # Build default pipe object
25
+ #
26
+ # This can be overridden in a custom changeset subclass
27
+ #
28
+ # @return [Pipe]
29
+ def self.default_pipe
30
+ Pipe.new
31
+ end
32
+
33
+ # @api private
34
+ def initialize(relation, data, options = EMPTY_HASH)
35
+ @relation = relation
36
+ @data = data
37
+ super
38
+ end
39
+
40
+ # Pipe changeset's data using custom steps define on the pipe
41
+ #
42
+ # @param *steps [Array<Symbol>] A list of mapping steps
43
+ #
44
+ # @return [Changeset]
45
+ #
46
+ # @api public
47
+ def map(*steps)
48
+ with(pipe: steps.reduce(pipe) { |a, e| a >> pipe.class[e] })
49
+ end
50
+
51
+ # Coerce changeset to a hash
52
+ #
53
+ # This will send the data through the pipe
54
+ #
55
+ # @return [Hash]
56
+ #
57
+ # @api public
58
+ def to_h
59
+ pipe.call(data)
60
+ end
61
+ alias_method :to_hash, :to_h
62
+
63
+ # Return a new changeset with updated options
64
+ #
65
+ # @param [Hash] new_options The new options
66
+ #
67
+ # @return [Changeset]
68
+ #
69
+ # @api private
70
+ def with(new_options)
71
+ self.class.new(relation, data, options.merge(new_options))
72
+ end
73
+
74
+ private
75
+
76
+ # @api private
77
+ def respond_to_missing?(meth, include_private = false)
78
+ super || data.respond_to?(meth)
79
+ end
80
+
81
+ # @api private
82
+ def method_missing(meth, *args, &block)
83
+ if data.respond_to?(meth)
84
+ response = data.__send__(meth, *args, &block)
85
+
86
+ if response.is_a?(Hash)
87
+ self.class.new(relation, response, options)
88
+ else
89
+ response
90
+ end
91
+ else
92
+ super
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ require 'rom/repository/changeset/create'
99
+ require 'rom/repository/changeset/update'
@@ -0,0 +1,142 @@
1
+ module ROM
2
+ class Repository
3
+ # Class-level APIs for repositories
4
+ #
5
+ # @api public
6
+ module ClassInterface
7
+ # Create a root-repository class and set its root relation
8
+ #
9
+ # @example
10
+ # # where :users is the relation name in your rom container
11
+ # class UserRepo < ROM::Repository[:users]
12
+ # end
13
+ #
14
+ # @param name [Symbol] The relation `register_as` value
15
+ #
16
+ # @return [Class] descendant of ROM::Repository::Root
17
+ #
18
+ # @api public
19
+ def [](name)
20
+ klass = Class.new(self < Repository::Root ? self : Repository::Root)
21
+ klass.relations(name)
22
+ klass.root(name)
23
+ klass
24
+ end
25
+
26
+ # Inherits configured relations and commands
27
+ #
28
+ # @api private
29
+ def inherited(klass)
30
+ super
31
+
32
+ return if self === Repository
33
+
34
+ klass.relations(*relations)
35
+ klass.commands(*commands)
36
+ end
37
+
38
+ # Define which relations your repository is going to use
39
+ #
40
+ # @example
41
+ # class MyRepo < ROM::Repository::Base
42
+ # relations :users, :tasks
43
+ # end
44
+ #
45
+ # my_repo = MyRepo.new(rom)
46
+ #
47
+ # my_repo.users
48
+ # my_repo.tasks
49
+ #
50
+ # @return [Array<Symbol>]
51
+ #
52
+ # @api public
53
+ def relations(*names)
54
+ if names.any?
55
+ attr_reader(*names)
56
+
57
+ if defined?(@relations)
58
+ @relations.concat(names).uniq!
59
+ else
60
+ @relations = names
61
+ end
62
+
63
+ @relations
64
+ else
65
+ @relations
66
+ end
67
+ end
68
+
69
+ # Defines command methods on a root repository
70
+ #
71
+ # @example
72
+ # class UserRepo < ROM::Repository[:users]
73
+ # commands :create, update: :by_pk, delete: :by_pk
74
+ # end
75
+ #
76
+ # # with custom command plugin
77
+ # class UserRepo < ROM::Repository[:users]
78
+ # commands :create, plugin: :my_command_plugin
79
+ # end
80
+ #
81
+ # # with custom mapper
82
+ # class UserRepo < ROM::Repository[:users]
83
+ # commands :create, mapper: :my_custom_mapper
84
+ # end
85
+ #
86
+ # @param *names [Array<Symbol>] A list of command names
87
+ # @option :mapper [Symbol] An optional mapper identifier
88
+ # @option :use [Symbol] An optional command plugin identifier
89
+ #
90
+ # @return [Array<Symbol>] A list of defined command names
91
+ #
92
+ # @api public
93
+ def commands(*names, mapper: nil, use: nil, **opts)
94
+ if names.any? || opts.any?
95
+ @commands = names + opts.to_a
96
+
97
+ @commands.each do |spec|
98
+ type, *view = Array(spec).flatten
99
+
100
+ if view.size > 0
101
+ define_restricted_command_method(type, view, mapper: mapper, use: use)
102
+ else
103
+ define_command_method(type, mapper: mapper, use: use)
104
+ end
105
+ end
106
+ else
107
+ @commands || []
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # @api private
114
+ def define_command_method(type, **opts)
115
+ define_method(type) do |*args|
116
+ command(type => self.class.root, **opts).call(*args)
117
+ end
118
+ end
119
+
120
+ # @api private
121
+ def define_restricted_command_method(type, views, **opts)
122
+ views.each do |view_name|
123
+ meth_name = views.size > 1 ? :"#{type}_#{view_name}" : type
124
+
125
+ define_method(meth_name) do |*args|
126
+ view_args, *input = args
127
+
128
+ changeset = input.first
129
+
130
+ if changeset.is_a?(Changeset) && changeset.clean?
131
+ map_tuple(changeset.relation, changeset.original)
132
+ else
133
+ command(type => self.class.root, **opts)
134
+ .public_send(view_name, *view_args)
135
+ .call(*input)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end