rom-repository 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order random
3
+ --require ./spec/spec_helper
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 Version](https://badge.fury.io/rb/rom-repository.svg)][gem]
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]
12
+ [![Code Climate](https://codeclimate.com/github/rom-rb/rom-repository/badges/gpa.svg)][codeclimate]
13
+ [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
14
+ [![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][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,2 @@
1
+ require 'rom'
2
+ require 'rom/repository/base'
@@ -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