appfuel 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +78 -1
- data/appfuel.gemspec +1 -0
- data/docs/images/appfuel_basic_flow.png +0 -0
- data/lib/appfuel/application/app_container.rb +24 -1
- data/lib/appfuel/application/container_class_registration.rb +29 -4
- data/lib/appfuel/application/root.rb +1 -0
- data/lib/appfuel/application.rb +0 -1
- data/lib/appfuel/domain/base_criteria.rb +171 -0
- data/lib/appfuel/domain/criteria_builder.rb +248 -0
- data/lib/appfuel/domain/criteria_settings.rb +156 -0
- data/lib/appfuel/domain/dsl.rb +5 -1
- data/lib/appfuel/domain/entity.rb +1 -2
- data/lib/appfuel/domain/exists_criteria.rb +57 -0
- data/lib/appfuel/domain/expr.rb +66 -97
- data/lib/appfuel/domain/expr_conjunction.rb +27 -0
- data/lib/appfuel/domain/expr_parser.rb +199 -0
- data/lib/appfuel/domain/expr_transform.rb +68 -0
- data/lib/appfuel/domain/search_criteria.rb +137 -0
- data/lib/appfuel/domain.rb +6 -1
- data/lib/appfuel/feature/initializer.rb +5 -0
- data/lib/appfuel/handler/action.rb +3 -0
- data/lib/appfuel/handler/base.rb +11 -1
- data/lib/appfuel/handler/command.rb +4 -0
- data/lib/appfuel/repository/base.rb +16 -2
- data/lib/appfuel/repository/mapper.rb +41 -7
- data/lib/appfuel/repository/mapping_dsl.rb +4 -4
- data/lib/appfuel/repository/mapping_entry.rb +2 -2
- data/lib/appfuel/repository.rb +0 -1
- data/lib/appfuel/storage/db/active_record_model.rb +32 -28
- data/lib/appfuel/storage/db/mapper.rb +38 -125
- data/lib/appfuel/storage/db/repository.rb +6 -10
- data/lib/appfuel/storage/memory/repository.rb +4 -0
- data/lib/appfuel/types.rb +0 -1
- data/lib/appfuel/version.rb +1 -1
- data/lib/appfuel.rb +6 -10
- metadata +26 -7
- data/lib/appfuel/application/container_key.rb +0 -201
- data/lib/appfuel/application/qualify_container_key.rb +0 -76
- data/lib/appfuel/db_model.rb +0 -16
- data/lib/appfuel/domain/criteria.rb +0 -436
- data/lib/appfuel/repository/mapping_registry.rb +0 -121
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6aa8eb41aca3f2b82a2b9affbb29399c8407a1ed
|
4
|
+
data.tar.gz: 9edc5e0b91bf9acde76210f9e0b1fd84b5d96f78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f779a784215090476418ff5932c6a8b26ad625805494e7851ae469ec4c1b8da5cc452daa7a6815d270fc3a1f82f65e056d50e4139bd23101e738aad8a9f0634
|
7
|
+
data.tar.gz: 57867104cf6bc9650d2c7b17ac45547222893ca43ee8c67add88cc9d878ccfd4283d69a5abdb81d1b606b6f711243305a8965298ed75707bb7ac9e7736e56cae
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file. (Pending approval) This project adheres to [Semantic Versioning](http://semver.org/). You can read more on this at [keep a change log](http://keepachangelog.com/)
|
3
|
+
|
4
|
+
# [Unreleased]
|
5
|
+
|
6
|
+
# Releases
|
7
|
+
## [[0.2.5]](https://github.com/rsb/appfuel/releases/tag/0.2.4) 2017-05-23
|
8
|
+
### Changed
|
9
|
+
- `Appfuel::Domain::BaseCriteria` can now qualify `domain exprs`
|
10
|
+
- refactored and tested repo mapper
|
11
|
+
- starting to work on db mapper interfaces
|
12
|
+
|
13
|
+
|
14
|
+
### Added
|
15
|
+
- added exists interface to db mapper, will finalize the repo on next release
|
data/README.md
CHANGED
@@ -23,7 +23,84 @@ Or install it yourself as:
|
|
23
23
|
|
24
24
|
$ gem install appfuel
|
25
25
|
|
26
|
-
##
|
26
|
+
## Overview
|
27
|
+
Appfuel exists to solve two problems, aligning application boundaries & code scalability.
|
28
|
+
|
29
|
+
### Application Boundaries
|
30
|
+
We align our application boundaries by isolating our business logic into its own gem and exposing a set of interfaces to interact with that logic. Your ruby app (web, console, daemon) would represent its own boundary that simply interacted with your business gem. It would do this by declaring the business gem as dependency it needs to use and treating it like any other gem. As a result, we establish a standard set of inputs that always deliver a predictable output.
|
31
|
+
|
32
|
+
### Code Scalability
|
33
|
+
We address the issue of how well code scales by using object oriented design principles, mainly, `single point of responsibility`. This however, creates an illusion, instead of having a small number of very large files, we produce a large number of smaller files. While we have more files, they are simpler to read and mentally reason their intent.
|
34
|
+
|
35
|
+
We also tackle complexity using `dependency inversion` where we build all of our dependencies into an application container and rely on the fact that every dependency can be found using a key that will resolve to that particular dependency.
|
36
|
+
|
37
|
+
We will cover the basic here, more detailed docs can be found on our [gitbook](https://rsb.gitbooks.io/appfuel/).
|
38
|
+
|
39
|
+
## Architecture
|
40
|
+
![Basic Flow](docs/images/appfuel_basic_flow.png)
|
41
|
+
|
42
|
+
## Directory Structure, Ruby Namespaces & the Application Container
|
43
|
+
This is not required but we assume your are moving your business code behind a `rubygem` and as such you should be following [rubygems guidelines](http://guides.rubygems.org/patterns/). From the gem's `lib` we usually declare the root module as the gem module. For example for a gem named `FooBar` we have a lib director that looks like:
|
44
|
+
```
|
45
|
+
$ ls
|
46
|
+
foo_bar/ foo_bar.rb
|
47
|
+
```
|
48
|
+
|
49
|
+
We loosely follow the guideline that directories match namespaces, however to try and prevent overly long namespaces where we can, we sometimes break with this guideline.
|
50
|
+
|
51
|
+
All Appfuel dependencies are registered into a dependency injection (Inversion of Control IoC) container. There are two general types of dependencies:
|
52
|
+
|
53
|
+
1. Classes
|
54
|
+
- `Actions`, `Commands`, `Repositories` & `Domains`
|
55
|
+
- Auto register with container using its ruby namespace with a few rules.
|
56
|
+
- Container Key Rules:
|
57
|
+
- the root namespace is never in the key, it is the name of the container
|
58
|
+
- the module directly under the root is either `global` or `the name of a feature module`
|
59
|
+
- ex) `FooBar::Global::Search` => `global.actions.search`
|
60
|
+
- ex) `FooBar::Users::Search` => `features.users.actions.search`
|
61
|
+
|
62
|
+
2. Blocks defined from a DSL
|
63
|
+
- `initializers`, `validators`, `domain builders` & `presenters`
|
64
|
+
- register with container via DSL.
|
65
|
+
- DSL handles registration key
|
66
|
+
|
67
|
+
## Setting Up Appfuel
|
68
|
+
In order to use Appfuel we mixin `Appfuel::Application::Root` into our `application root class` and configure it as follows:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
module FooBar
|
72
|
+
extend Appfuel::Application::Root
|
73
|
+
|
74
|
+
setup_appfuel root: self,
|
75
|
+
root_path: File.dirname(__FILE__),
|
76
|
+
config_definition: Configuration.definition,
|
77
|
+
on_after_setup: ->(_container) {
|
78
|
+
require_relative 'foo_bar/initializers'
|
79
|
+
}
|
80
|
+
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
1. Mixin the application root to allow us to use `setup_appfuel` and `call`
|
85
|
+
- `call` with run any action with a route of format `feature/action`
|
86
|
+
2. `setup_appfuel` allows `appfuel` configure & initialize your system
|
87
|
+
- `root`: is the root module, its name with be the name of the `app_container`
|
88
|
+
- `root_path`: allows appfuel to auto load features
|
89
|
+
- `config_definition`: is a Dsl that setups configuration data
|
90
|
+
- `on_after_setup`: is a hook that will call your lambda once setup is done
|
91
|
+
|
92
|
+
After `setup_appfuel` is called the application container named `foo_bar` is initialized and ready. To retrieve the container from `appfuel`:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Appfuel.app_container('foo_bar')
|
96
|
+
```
|
97
|
+
|
98
|
+
Since there are no other app container this is also the default container so you can simply call:
|
99
|
+
```ruby
|
100
|
+
Appfuel.app_container
|
101
|
+
```
|
102
|
+
This is will return the same container as above.
|
103
|
+
|
27
104
|
|
28
105
|
|
29
106
|
## Development
|
data/appfuel.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_dependency "dry-validation", "~> 0.10.5"
|
30
30
|
spec.add_dependency "dry-monads", "~> 0.2"
|
31
31
|
spec.add_dependency "dry-configurable", "~> 0.6"
|
32
|
+
spec.add_dependency "parslet", "~> 1.8.0"
|
32
33
|
|
33
34
|
spec.add_development_dependency "bundler", "~> 1.13"
|
34
35
|
spec.add_development_dependency "rake", "~> 10.0"
|
Binary file
|
@@ -40,6 +40,7 @@ module Appfuel
|
|
40
40
|
def container_path?
|
41
41
|
!@container_path.nil?
|
42
42
|
end
|
43
|
+
|
43
44
|
# @param list [Array] list of container namespace parts including root
|
44
45
|
# @return [Array]
|
45
46
|
def container_path=(list)
|
@@ -113,6 +114,28 @@ module Appfuel
|
|
113
114
|
@container_path.last
|
114
115
|
end
|
115
116
|
|
117
|
+
# Allow the concrete class that is using this mixing to change the
|
118
|
+
# key which would otherwise be the lowercase, underscored class name
|
119
|
+
#
|
120
|
+
# @return nil
|
121
|
+
def container_class_key(value = nil)
|
122
|
+
return @container_class_key if value.nil?
|
123
|
+
p "[container-class-key] #{self} => #{value}"
|
124
|
+
@container_class_key = value
|
125
|
+
end
|
126
|
+
|
127
|
+
def container_class_id
|
128
|
+
container_class_key || container_key_basename
|
129
|
+
end
|
130
|
+
|
131
|
+
def container_class_type
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def container_class_path
|
136
|
+
"#{top_container_key}.#{container_class_type}.#{container_class_id}"
|
137
|
+
end
|
138
|
+
|
116
139
|
# Fully qualified key, meaning you can access the class this was mixed into
|
117
140
|
# if you stored it into the container using this key
|
118
141
|
#
|
@@ -135,6 +158,7 @@ module Appfuel
|
|
135
158
|
@container_global_name ||= 'global'
|
136
159
|
end
|
137
160
|
|
161
|
+
#
|
138
162
|
# Convert the injection key to a fully qualified namespaced key that
|
139
163
|
# is used to pull an item out of the app container.
|
140
164
|
#
|
@@ -177,7 +201,6 @@ module Appfuel
|
|
177
201
|
# convert_to_qualified_container_key('global.baz', 'container')
|
178
202
|
#
|
179
203
|
# returns 'baz'
|
180
|
-
#
|
181
204
|
# @param key [String] partial key to be built into fully qualified key
|
182
205
|
# @param type_ns [String] namespace for key
|
183
206
|
# @return [String] fully qualified namespaced key
|
@@ -7,16 +7,41 @@ module Appfuel
|
|
7
7
|
# container key, so we simply need to obtain the qualified namespace
|
8
8
|
# key for this class extending this, that does not belong to appfuel.
|
9
9
|
#
|
10
|
+
# types of classes:
|
11
|
+
# repositories
|
12
|
+
# db
|
13
|
+
# domains
|
14
|
+
#
|
15
|
+
# features.repositories.key
|
10
16
|
# @param klass [Class] the handler class that is inheriting this
|
11
17
|
# @return [Boolean]
|
12
|
-
def
|
18
|
+
def stage_class_for_registration(klass)
|
19
|
+
if !klass.respond_to?(:register_class?) || !klass.register_class?
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
unless klass.respond_to?(:container_root_name)
|
24
|
+
fail "#{klass} must implement :container_root_name"
|
25
|
+
end
|
13
26
|
root = klass.container_root_name
|
14
27
|
return false if root == 'appfuel'
|
15
28
|
|
16
|
-
container = Appfuel.app_container(
|
17
|
-
container
|
18
|
-
true
|
29
|
+
container = Appfuel.app_container(klass.container_root_name)
|
30
|
+
container[:auto_register_classes] << klass
|
19
31
|
end
|
32
|
+
|
33
|
+
def disable_class_registration
|
34
|
+
@is_class_registration = false
|
35
|
+
end
|
36
|
+
|
37
|
+
def enable_class_registration
|
38
|
+
@is_class_registration = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def register_class?
|
42
|
+
@is_class_registration ||= true
|
43
|
+
end
|
44
|
+
|
20
45
|
end
|
21
46
|
end
|
22
47
|
end
|
@@ -109,6 +109,7 @@ module Appfuel
|
|
109
109
|
container.register(:root, root)
|
110
110
|
container.register(:root_name, root_name)
|
111
111
|
container.register(:root_path, root_path)
|
112
|
+
container.register(:auto_register_classes, [])
|
112
113
|
container.register(:repository_mappings, {})
|
113
114
|
container.register(:repository_initializer, repo_initializer)
|
114
115
|
container.register(:features_path, "#{root_name}/features")
|
data/lib/appfuel/application.rb
CHANGED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Domain
|
3
|
+
|
4
|
+
# The Criteria represents the interface between the repositories and actions
|
5
|
+
# or commands. The allow you to find entities in the application storage (
|
6
|
+
# a database) without knowledge of that storage system. The criteria will
|
7
|
+
# always refer to its queries in the domain language for which the repo is
|
8
|
+
# responsible for mapping that query to its persistence layer.
|
9
|
+
#
|
10
|
+
# global.user
|
11
|
+
# memberships.user
|
12
|
+
#
|
13
|
+
# exist: 'foo.bar exists id = 6'
|
14
|
+
# search: 'foo.bar filter id = 6 and bar = "foo" order id asc limit 6'
|
15
|
+
#
|
16
|
+
# search:
|
17
|
+
# domain: 'foo.bar',
|
18
|
+
#
|
19
|
+
# filters: 'id = 6 or id = 8 and id = 9'
|
20
|
+
# filters: [
|
21
|
+
# 'id = 6',
|
22
|
+
# {or: 'id = 8'}
|
23
|
+
# {and: id = 9'}
|
24
|
+
# ]
|
25
|
+
#
|
26
|
+
# order: 'foo.bar.id asc'
|
27
|
+
# order: 'foo.bar.id'
|
28
|
+
# order: [
|
29
|
+
# 'foo.bar.id',
|
30
|
+
# {desc: 'foo.bar.id'},
|
31
|
+
# {asc: 'foo.bar.id'}
|
32
|
+
# ]
|
33
|
+
# limit: 1
|
34
|
+
#
|
35
|
+
# settings:
|
36
|
+
# page: 1
|
37
|
+
# per_page: 2
|
38
|
+
# disable_pagination
|
39
|
+
# first
|
40
|
+
# all
|
41
|
+
# last
|
42
|
+
# error_on_empty
|
43
|
+
# parser
|
44
|
+
# transform
|
45
|
+
#
|
46
|
+
# exists:
|
47
|
+
# domain:
|
48
|
+
# expr:
|
49
|
+
#
|
50
|
+
#
|
51
|
+
class BaseCriteria
|
52
|
+
include DomainNameParser
|
53
|
+
|
54
|
+
|
55
|
+
attr_reader :domain_basename, :domain_name, :feature, :settings, :filters
|
56
|
+
|
57
|
+
# Parse out the domain into feature, domain, determine the name of the
|
58
|
+
# repo this criteria is for and initailize basic settings.
|
59
|
+
# global.user
|
60
|
+
#
|
61
|
+
# membership.user
|
62
|
+
# foo.id filter name like "foo" order foo.bar.id asc limit 2
|
63
|
+
# foo.id exists foo.id = 5
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# SpCore::Domain::Criteria('foo', single: true)
|
67
|
+
# Types.Criteria('foo.bar', single: true)
|
68
|
+
#
|
69
|
+
# === Options
|
70
|
+
# error_on_empty: will cause the repo to fail when query returns an
|
71
|
+
# an empty dataset. The failure will have the message
|
72
|
+
# with key as domain and text is "<domain> not found"
|
73
|
+
#
|
74
|
+
# single: will cause the repo to return only one, the first,
|
75
|
+
# entity in the dataset
|
76
|
+
#
|
77
|
+
# @param domain [String] fully qualified domain name
|
78
|
+
# @param opts [Hash] options for initializing criteria
|
79
|
+
# @return [Criteria]
|
80
|
+
def initialize(domain_name, data = {})
|
81
|
+
@feature, @domain_basename, @domain_name = parse_domain_name(domain_name)
|
82
|
+
@settings = data[:settings] || CriteriaSettings.new(data)
|
83
|
+
@filters = nil
|
84
|
+
@params = {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def clear_filters
|
88
|
+
@filters = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def filters?
|
92
|
+
!filters.nil?
|
93
|
+
end
|
94
|
+
|
95
|
+
def global?
|
96
|
+
!feature?
|
97
|
+
end
|
98
|
+
|
99
|
+
def feature?
|
100
|
+
@feature != 'global'
|
101
|
+
end
|
102
|
+
|
103
|
+
# @example
|
104
|
+
# criteria.add_param('foo', 100)
|
105
|
+
#
|
106
|
+
# @param key [Symbol, String] The key name where we want to keep the value
|
107
|
+
# @param value [String, Integer] The value that belongs to the key param
|
108
|
+
# @return [String, Integer] The saved value
|
109
|
+
def add_param(key, value)
|
110
|
+
fail 'key should not be nil' if key.nil?
|
111
|
+
|
112
|
+
@params[key.to_sym] = value
|
113
|
+
end
|
114
|
+
|
115
|
+
# @param key [String, Symbol]
|
116
|
+
# @return [String, Integer, Boolean] the found value
|
117
|
+
def param(key)
|
118
|
+
@params[key.to_sym]
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param key [String, Symbol]
|
122
|
+
# @return [Boolean]
|
123
|
+
def param?(key)
|
124
|
+
@params.key?(key.to_sym)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @return [Boolean] if the @params variable has values
|
128
|
+
def params?
|
129
|
+
!@params.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
def parse_expr(str)
|
134
|
+
if !(settings.parser && settings.parser.respond_to?(:parse))
|
135
|
+
fail "expression parser must implement to :parse"
|
136
|
+
end
|
137
|
+
|
138
|
+
if !(settings.transform && settings.transform.respond_to?(:apply))
|
139
|
+
fail "expression transform must implement :apply"
|
140
|
+
end
|
141
|
+
|
142
|
+
begin
|
143
|
+
tree = settings.parser.parse(str)
|
144
|
+
rescue Parslet::ParseFailed => e
|
145
|
+
msg = "The expression (#{str}) failed to parse"
|
146
|
+
err = RuntimeError.new(msg)
|
147
|
+
err.set_backtrace(e.backtrace)
|
148
|
+
raise err
|
149
|
+
end
|
150
|
+
|
151
|
+
result = settings.transform.apply(tree)
|
152
|
+
result = result[:domain_expr] || result[:root]
|
153
|
+
unless result
|
154
|
+
fail "unable to parse (#{str}) correctly"
|
155
|
+
end
|
156
|
+
result
|
157
|
+
end
|
158
|
+
|
159
|
+
def qualify_expr(domain_expr)
|
160
|
+
return domain_expr if domain_expr.qualified?
|
161
|
+
if global?
|
162
|
+
domain_expr.qualify_global(domain_basename)
|
163
|
+
return domain_expr
|
164
|
+
end
|
165
|
+
|
166
|
+
domain_expr.qualify_feature(feature, domain_basename)
|
167
|
+
domain_expr
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module SpCore
|
2
|
+
module Domain
|
3
|
+
|
4
|
+
# CriteriaBuilder is an interface creating new Criteria as well as
|
5
|
+
# building up an existing Criteria instance
|
6
|
+
class CriteriaBuilder
|
7
|
+
|
8
|
+
# For the list of defined inputs and maps, it builds new Criteria object
|
9
|
+
# following build pattern
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# inputs = {
|
14
|
+
# search: "projects.offer",
|
15
|
+
# repo_map: { "projects.offer" => "foo" },
|
16
|
+
# filters: [
|
17
|
+
# {foo: 'bar', ep: 1}
|
18
|
+
# ],
|
19
|
+
# order: [
|
20
|
+
# {created_at: :desc}
|
21
|
+
# ],
|
22
|
+
# limit: 5,
|
23
|
+
# all: true,
|
24
|
+
# page: 1,
|
25
|
+
# per_page: 10
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# maps = {
|
29
|
+
# feature: "projects",
|
30
|
+
# attr_map: {
|
31
|
+
# foo: "users.user.foo"
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
#
|
35
|
+
# criteria_builder = SpCore::Domain::CriteriaBuilder.build(inputs, maps)
|
36
|
+
#
|
37
|
+
# @param inputs [Hash] input data
|
38
|
+
# @param maps [Hash] mapping data
|
39
|
+
# @return [Criteria]
|
40
|
+
def self.build(inputs, maps = {})
|
41
|
+
builder = new(inputs, maps)
|
42
|
+
criteria = builder.create
|
43
|
+
builder.build(criteria, inputs)
|
44
|
+
criteria
|
45
|
+
end
|
46
|
+
|
47
|
+
# Builds new Criteria object
|
48
|
+
#
|
49
|
+
# @param inputs [Hash] input data
|
50
|
+
# @option inputs [String] :search entity/domain to search for
|
51
|
+
# @option inputs [Hash] :repo_map repository mapping by entity/domain
|
52
|
+
# @option inputs [Array] :filters list of filter items
|
53
|
+
# @option inputs [Array] :order list of order definitions
|
54
|
+
# @option inputs [Integer] :limit number of records to limit to
|
55
|
+
# @option inputs [Boolean] :all flag for returning all records
|
56
|
+
# @option inputs [Boolean] :first flag for returning only first record
|
57
|
+
# @option inputs [Boolean] :last flag for returning only last record
|
58
|
+
# @option inputs [Integer] :page number of the page in collection
|
59
|
+
# @option inputs [Integer] :per_page num. of items per page in collection
|
60
|
+
# @option inputs [Boolean] :single_page flag for getting single page only
|
61
|
+
# @param maps [Hash] mapping data
|
62
|
+
# @option maps [Hash] :attr_map mapping by attr to domain.attribute_name
|
63
|
+
# @return [Criteria]
|
64
|
+
def initialize(inputs, maps)
|
65
|
+
domain_name = inputs.fetch(:search).to_s
|
66
|
+
repo_map = inputs[:repo_map] || {}
|
67
|
+
fail ":repo_map must be a Hash" unless repo_map.is_a?(Hash)
|
68
|
+
|
69
|
+
@attr_map = maps[:attr_map] || {}
|
70
|
+
@domain_name = "#{domain_name}"
|
71
|
+
@repo_name = repo(repo_map, domain_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Creates new Criteria instance.
|
75
|
+
# Actually it's just a delegator to Criteria initializer
|
76
|
+
#
|
77
|
+
# @param domain [String] fully qualified domain name
|
78
|
+
# @param options [Hash] options for initializing Criteria
|
79
|
+
# @return [Criteria]
|
80
|
+
def create
|
81
|
+
Criteria.new(domain_name, repo: repo_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Build criteria clauses
|
85
|
+
#
|
86
|
+
# @param inputs [Hash]
|
87
|
+
def build(criteria, inputs)
|
88
|
+
self.filters(criteria, inputs, attr_map)
|
89
|
+
.order(criteria, inputs, attr_map)
|
90
|
+
.limit(criteria, inputs)
|
91
|
+
.scope(criteria, inputs)
|
92
|
+
.pagination(criteria, inputs)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Updates Criteria object filters with the list of given filters
|
96
|
+
# Each filter item must be a Hash.
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# inputs = {
|
100
|
+
# filters: [
|
101
|
+
# {first_name: 'Bob'},
|
102
|
+
# {last_name: 'Doe', op: 'like'},
|
103
|
+
# {last_name: 'Johnson', op: 'like', or: false}
|
104
|
+
# ]
|
105
|
+
# }
|
106
|
+
#
|
107
|
+
# @param criteria [Criteria] criteria object to build
|
108
|
+
# @param inputs [Hash] input data
|
109
|
+
# @option inputs [Array] :filters list of filter items
|
110
|
+
# @param attr_map [Hash] mapping by attr to domain.attribute_name
|
111
|
+
# @return [CriteriaBuilder]
|
112
|
+
def filters(criteria, inputs, attr_map = {})
|
113
|
+
if inputs.key?(:filters)
|
114
|
+
filters = inputs.fetch(:filters)
|
115
|
+
fail ":filters must be an Array" unless filters.is_a?(Array)
|
116
|
+
|
117
|
+
filters.each do |filter_item|
|
118
|
+
attr_name, value = filter_item.first
|
119
|
+
qualified_name = lookup_attribute(attr_name, attr_map)
|
120
|
+
if qualified_name != attr_name
|
121
|
+
filter_item.delete(attr_name)
|
122
|
+
filter_item[qualified_name] = value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
criteria.filter(filters)
|
127
|
+
end
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
# Updates given Criteria ordering clause.
|
132
|
+
# It expects a list of order definitions.
|
133
|
+
# Order definition might be either Hash, String or Symbol.
|
134
|
+
# If item is String or Symbol, ordering direction is ascending(:asc)
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# inputs = {
|
138
|
+
# order: [
|
139
|
+
# {created_at: :desc},
|
140
|
+
# 'created_at',
|
141
|
+
# :created_at
|
142
|
+
# ]
|
143
|
+
# }
|
144
|
+
#
|
145
|
+
# @param criteria [Criteria] criteria object to build
|
146
|
+
# @param inputs [Hash] input data
|
147
|
+
# @option inputs [Array] :order list of order definitions
|
148
|
+
# @param attr_map [Hash] mapping by attr to domain.attribute_name
|
149
|
+
# @return [CriteriaBuilder]
|
150
|
+
def order(criteria, inputs, attr_map = {})
|
151
|
+
return self unless inputs.key?(:order)
|
152
|
+
list = inputs.fetch(:order)
|
153
|
+
|
154
|
+
fail ":order must implement :each" unless list.respond_to?(:each)
|
155
|
+
return self if list.empty?
|
156
|
+
|
157
|
+
final_order = {}
|
158
|
+
list.each do |order|
|
159
|
+
domain_attr_name = order
|
160
|
+
order_dir = "asc"
|
161
|
+
|
162
|
+
if order.is_a?(Hash)
|
163
|
+
domain_attr_name, order_dir = order.first
|
164
|
+
end
|
165
|
+
|
166
|
+
qualified_name = lookup_attribute(domain_attr_name, attr_map)
|
167
|
+
final_order[qualified_name.to_s] = order_dir.to_s
|
168
|
+
criteria.order_by(final_order)
|
169
|
+
end
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# Updates given Criteria record limit
|
174
|
+
#
|
175
|
+
# @param criteria [Criteria] criteria to build
|
176
|
+
# @param inputs [Hash] input data
|
177
|
+
# @option inputs [Integer] :limit number of records to limit to
|
178
|
+
# @return [CriteriaBuilder]
|
179
|
+
def limit(criteria, inputs)
|
180
|
+
if inputs.key?(:limit)
|
181
|
+
criteria.limit(inputs.fetch(:limit))
|
182
|
+
end
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
# Updates given Criteria collection limits.
|
187
|
+
# Those are :disable_pagination, :all, :first and :last properties.
|
188
|
+
#
|
189
|
+
# @param criteria [Criteria] criteria to build
|
190
|
+
# @param inputs [Hash] input data
|
191
|
+
# @option inputs [Boolean] :all flag for returning all records
|
192
|
+
# @option inputs [Boolean] :first flag for returning onnly first record
|
193
|
+
# @option inputs [Boolean] :last flag for returning only last record
|
194
|
+
# @return [CriteriaBuilder]
|
195
|
+
def scope(criteria, inputs)
|
196
|
+
if inputs[:first] == true
|
197
|
+
criteria.first
|
198
|
+
elsif inputs[:last] == true
|
199
|
+
criteria.last
|
200
|
+
elsif inputs[:disable_pagination] == true
|
201
|
+
criteria.disable_pagination
|
202
|
+
elsif inputs[:all] == true
|
203
|
+
criteria.all
|
204
|
+
end
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# Updates given Criteria pagination params.
|
209
|
+
#
|
210
|
+
# @param criteria [Criteria] criteria to build
|
211
|
+
# @param inputs [Hash] input data
|
212
|
+
# @option inputs [Integer] :page number of the page within collection
|
213
|
+
# @option inputs [Integer] :per_page number of items per page
|
214
|
+
# @option inputs [Boolean] :single_page flag for getting single page only
|
215
|
+
# @param attr_map [Hash] mapping by attr to domain.attribute_name
|
216
|
+
# @return [CriteriaBuilder]
|
217
|
+
def pagination(criteria, inputs)
|
218
|
+
criteria.disable_pagination if inputs[:single_page]==true || inputs[:disable_pagination] == true
|
219
|
+
criteria.page(inputs[:page]) if inputs[:page]
|
220
|
+
criteria.per_page(inputs[:per_page]) if inputs[:per_page]
|
221
|
+
self
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
attr_reader :attr_map, :domain_name, :repo_name
|
227
|
+
|
228
|
+
def repo(repo_map, domain_name)
|
229
|
+
repo_map.key?(domain_name) ? repo_map[domain_name].to_s : domain_name
|
230
|
+
end
|
231
|
+
|
232
|
+
# It looks for given attribute name in the mapping and returns
|
233
|
+
# domain.attribute_name string if found. Else it returns attribute
|
234
|
+
# name itself.
|
235
|
+
#
|
236
|
+
# @private
|
237
|
+
#
|
238
|
+
# @param name [String] attribute name
|
239
|
+
# @param map [Hash] attribute to domain.attribute_name mapping
|
240
|
+
# @return [String]
|
241
|
+
def lookup_attribute(name, map)
|
242
|
+
return name unless map.is_a?(Hash)
|
243
|
+
return name unless map.key?(name) || map.key?(name.to_sym)
|
244
|
+
map[name] || map[name.to_sym]
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|