rom-repository 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +22 -0
- data/README.md +136 -0
- data/Rakefile +18 -0
- data/lib/rom-repository.rb +2 -0
- data/lib/rom/repository/base.rb +56 -0
- data/lib/rom/repository/ext/relation.rb +125 -0
- data/lib/rom/repository/ext/relation/view_dsl.rb +33 -0
- data/lib/rom/repository/header_builder.rb +63 -0
- data/lib/rom/repository/loading_proxy.rb +173 -0
- data/lib/rom/repository/loading_proxy/combine.rb +158 -0
- data/lib/rom/repository/loading_proxy/wrap.rb +60 -0
- data/lib/rom/repository/mapper_builder.rb +30 -0
- data/lib/rom/repository/struct_builder.rb +38 -0
- data/lib/rom/repository/version.rb +9 -0
- data/lib/rom/struct.rb +32 -0
- data/rom-repository.gemspec +24 -0
- data/spec/integration/repository_spec.rb +40 -0
- data/spec/shared/database.rb +26 -0
- data/spec/shared/relations.rb +29 -0
- data/spec/shared/repo.rb +49 -0
- data/spec/shared/seeds.rb +11 -0
- data/spec/shared/structs.rb +103 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/mapper_registry.rb +11 -0
- data/spec/unit/header_builder_spec.rb +74 -0
- data/spec/unit/loading_proxy_spec.rb +138 -0
- data/spec/unit/sql/relation_spec.rb +48 -0
- data/spec/unit/struct_builder_spec.rb +25 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4ae5380662bd3fb3fb249bf893a491bb0accbb35
|
4
|
+
data.tar.gz: 43da8d866c970abe7d471a19ccee12d9882d6d87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c9fb3246025ba68d17f37e4364263975dba15776c9e516ea9caabb982a22b54555c9ab1ca8fb1eeaebae767b75c514b5aa2e06b3a550675e3cd4f7d23724b0f2
|
7
|
+
data.tar.gz: 0cde9b16532c6ab85b063480af8bacba6e05f668710752a3f94b07f4685ea0250a203dde2019565019dd3df6a258872085ebfb0fd16dc0ae49fc29356a501c75
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
4
|
+
bundler_args: --without yard guard benchmarks tools
|
5
|
+
before_script:
|
6
|
+
- psql -c 'create database rom;' -U postgres
|
7
|
+
script: "bundle exec rake ci"
|
8
|
+
rvm:
|
9
|
+
- 2.0
|
10
|
+
- 2.1
|
11
|
+
- 2.2
|
12
|
+
- rbx-2
|
13
|
+
- jruby
|
14
|
+
- jruby-head
|
15
|
+
- ruby-head
|
16
|
+
env:
|
17
|
+
global:
|
18
|
+
- CODECLIMATE_REPO_TOKEN=173b95dae5c6ac281bc36a4b212291a89fed9b520b39a86bafdf603692250e60
|
19
|
+
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
20
|
+
matrix:
|
21
|
+
allow_failures:
|
22
|
+
- rvm: ruby-head
|
23
|
+
- rvm: jruby-head
|
24
|
+
- rvm: rbx-2
|
25
|
+
notifications:
|
26
|
+
webhooks:
|
27
|
+
urls:
|
28
|
+
- https://webhooks.gitter.im/e/39e1225f489f38b0bd09
|
29
|
+
on_success: change
|
30
|
+
on_failure: always
|
31
|
+
on_start: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# v0.0.1 2015-08-05
|
2
|
+
|
3
|
+
First public release \o/
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
* `Relation#combine_parents` for auto-combine using eager-loading strategy aka
|
8
|
+
relation composition (solnic)
|
9
|
+
* `Relation#combine_children` for auto-combine using eager-loading strategy aka
|
10
|
+
relation composition (solnic)
|
11
|
+
* `Relation#wrap_parent` for auto-wrap using inner join (solnic)
|
12
|
+
* `Relation.view` for explicit relation view definitions with header and query (solnic)
|
13
|
+
* Auto-mapping feature which builds mappers that turn relations into simple structs (solnic)
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'rom', github: 'rom-rb/rom', branch: 'master'
|
6
|
+
gem 'rom-sql', github: 'rom-rb/rom-sql', branch: 'master'
|
7
|
+
gem 'inflecto'
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'rspec'
|
11
|
+
gem 'byebug', platforms: :mri
|
12
|
+
gem 'pg', platforms: [:mri, :rbx]
|
13
|
+
gem 'pg_jruby', platforms: :jruby
|
14
|
+
gem "codeclimate-test-reporter", require: nil
|
15
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Piotr Solnica
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
[gem]: https://rubygems.org/gems/rom-repository
|
2
|
+
[travis]: https://travis-ci.org/rom-rb/rom-repository
|
3
|
+
[gemnasium]: https://gemnasium.com/rom-rb/rom-repository
|
4
|
+
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-repository
|
5
|
+
[inchpages]: http://inch-ci.org/github/rom-rb/rom-repository
|
6
|
+
|
7
|
+
# ROM::Repository
|
8
|
+
|
9
|
+
[][gem]
|
10
|
+
[][travis]
|
11
|
+
[][gemnasium]
|
12
|
+
[][codeclimate]
|
13
|
+
[][codeclimate]
|
14
|
+
[][inchpages]
|
15
|
+
|
16
|
+
Repository for [ROM](https://github.com/rom-rb/rom) with auto-mapping and relation
|
17
|
+
extensions.
|
18
|
+
|
19
|
+
## Conventions & Definitions
|
20
|
+
|
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
|
+
### Decorating Structs
|
113
|
+
|
114
|
+
Nothing is stopping you from decorating your structs using registered mappers and
|
115
|
+
custom decorator models:
|
116
|
+
|
117
|
+
``` ruby
|
118
|
+
class UserStructMapper < ROM::Mapper
|
119
|
+
register_as :ui_presenter
|
120
|
+
model UI::UserPresenter
|
121
|
+
end
|
122
|
+
|
123
|
+
user_repo.by_id(1).as(:ui_presenter)
|
124
|
+
```
|
125
|
+
|
126
|
+
## Limitations
|
127
|
+
|
128
|
+
This is an early alpha and works only with rom-sql for now. There are a couple
|
129
|
+
improvements waiting to be done in the rom core and then rom-repository will receive
|
130
|
+
more love and features.
|
131
|
+
|
132
|
+
Stay tuned.
|
133
|
+
|
134
|
+
## License
|
135
|
+
|
136
|
+
See `LICENSE` file.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rspec/core/rake_task"
|
2
|
+
|
3
|
+
RSpec::Core::RakeTask.new(:spec)
|
4
|
+
task default: [:ci]
|
5
|
+
|
6
|
+
desc "Run CI tasks"
|
7
|
+
task ci: [:spec]
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "rubocop/rake_task"
|
11
|
+
|
12
|
+
Rake::Task[:default].enhance [:rubocop]
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new do |task|
|
15
|
+
task.options << "--display-cop-names"
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rom/support/options'
|
2
|
+
|
3
|
+
require 'rom/repository/ext/relation'
|
4
|
+
|
5
|
+
require 'rom/repository/mapper_builder'
|
6
|
+
require 'rom/repository/loading_proxy'
|
7
|
+
|
8
|
+
module ROM
|
9
|
+
class Repository < Gateway
|
10
|
+
# Abstract repository class to inherit from
|
11
|
+
#
|
12
|
+
# TODO: rename this to Repository once deprecated Repository from rom core is gone
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class Base # :trollface:
|
16
|
+
include Options
|
17
|
+
|
18
|
+
option :mapper_builder, reader: true, default: proc { MapperBuilder.new }
|
19
|
+
|
20
|
+
# Define which relations your repository is going to use
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class MyRepo < ROM::Repository::Base
|
24
|
+
# relations :users, :tasks
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# my_repo = MyRepo.new(rom_env)
|
28
|
+
#
|
29
|
+
# my_repo.users
|
30
|
+
# my_repo.tasks
|
31
|
+
#
|
32
|
+
# @return [Array<Symbol>]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def self.relations(*names)
|
36
|
+
if names.any?
|
37
|
+
attr_reader(*names)
|
38
|
+
@relations = names
|
39
|
+
else
|
40
|
+
@relations
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def initialize(env, options = {})
|
46
|
+
super
|
47
|
+
self.class.relations.each do |name|
|
48
|
+
proxy = LoadingProxy.new(
|
49
|
+
env.relation(name), name: name, mapper_builder: mapper_builder
|
50
|
+
)
|
51
|
+
instance_variable_set("@#{name}", proxy)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'rom/repository/ext/relation/view_dsl'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module SQL
|
5
|
+
# A bunch of extensions that will be ported to other adapters
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Relation < ROM::Relation
|
9
|
+
# @api private
|
10
|
+
def self.inherited(klass)
|
11
|
+
super
|
12
|
+
klass.class_eval do
|
13
|
+
exposed_relations.merge(Set[:columns, :for_combine, :for_wrap])
|
14
|
+
|
15
|
+
defines :attributes
|
16
|
+
attributes({})
|
17
|
+
|
18
|
+
option :attributes, reader: true, default: -> relation { relation.class.attributes }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define a relation view with a specific header
|
23
|
+
#
|
24
|
+
# With headers defined all the mappers will be inferred automatically
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# class Users < ROM::Relation[:sql]
|
28
|
+
# view(:by_name, [:id, :name]) do |name|
|
29
|
+
# where(name: name)
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# view(:listing, [:id, :name, :email]) do
|
33
|
+
# select(:id, :name, :email).order(:name)
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def self.view(*args, &block)
|
39
|
+
name, names, relation_block =
|
40
|
+
if block.arity == 0
|
41
|
+
ViewDSL.new(*args, &block).call
|
42
|
+
else
|
43
|
+
[*args, block]
|
44
|
+
end
|
45
|
+
|
46
|
+
attributes[name] = names
|
47
|
+
|
48
|
+
define_method(name, &relation_block)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return column names that will be selected for this relation
|
52
|
+
#
|
53
|
+
# By default we use dataset columns but first we look at configured
|
54
|
+
# attributes by `view` DSL
|
55
|
+
#
|
56
|
+
# @return [Array<Symbol>]
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
def columns
|
60
|
+
self.class.attributes.fetch(name, dataset.columns)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Default methods for fetching combined relation
|
64
|
+
#
|
65
|
+
# This method is used by default by `combine`
|
66
|
+
#
|
67
|
+
# @return [SQL::Relation]
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def for_combine(keys, relation)
|
71
|
+
pk, fk = keys.to_a.flatten
|
72
|
+
where(fk => relation.map { |tuple| tuple[pk] })
|
73
|
+
end
|
74
|
+
|
75
|
+
# Default methods for fetching wrapped relation
|
76
|
+
#
|
77
|
+
# This method is used by default by `wrap` and `wrap_parents`
|
78
|
+
#
|
79
|
+
# @return [SQL::Relation]
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
def for_wrap(name, keys)
|
83
|
+
other = __registry__[name]
|
84
|
+
|
85
|
+
inner_join(name, keys)
|
86
|
+
.select(*qualified.header.columns)
|
87
|
+
.select_append(*other.prefix(other.name).qualified.header)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Infer foreign_key name for this relation
|
91
|
+
#
|
92
|
+
# TODO: this should be configurable and handled by an injected policy
|
93
|
+
#
|
94
|
+
# @return [Symbol]
|
95
|
+
#
|
96
|
+
# @api private
|
97
|
+
def foreign_key
|
98
|
+
:"#{Inflector.singularize(name)}_id"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO: remove this once Relation::Lazy is gone
|
104
|
+
class Relation
|
105
|
+
class Lazy
|
106
|
+
def base_name
|
107
|
+
relation.name
|
108
|
+
end
|
109
|
+
|
110
|
+
def primary_key
|
111
|
+
relation.primary_key
|
112
|
+
end
|
113
|
+
|
114
|
+
def foreign_key
|
115
|
+
relation.foreign_key
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class Curried < Lazy
|
120
|
+
def columns
|
121
|
+
relation.attributes.fetch(options[:name], relation.columns)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|