rom-repository 0.2.0 → 0.3.0

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