appfuel 0.2.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +19 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/appfuel.gemspec +42 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/appfuel.rb +210 -0
- data/lib/appfuel/application.rb +4 -0
- data/lib/appfuel/application/app_container.rb +223 -0
- data/lib/appfuel/application/container_class_registration.rb +22 -0
- data/lib/appfuel/application/container_key.rb +201 -0
- data/lib/appfuel/application/qualify_container_key.rb +76 -0
- data/lib/appfuel/application/root.rb +140 -0
- data/lib/appfuel/cli_msg_request.rb +19 -0
- data/lib/appfuel/configuration.rb +14 -0
- data/lib/appfuel/configuration/definition_dsl.rb +175 -0
- data/lib/appfuel/configuration/file_loader.rb +61 -0
- data/lib/appfuel/configuration/populate.rb +95 -0
- data/lib/appfuel/configuration/search.rb +45 -0
- data/lib/appfuel/db_model.rb +16 -0
- data/lib/appfuel/domain.rb +7 -0
- data/lib/appfuel/domain/criteria.rb +436 -0
- data/lib/appfuel/domain/domain_name_parser.rb +44 -0
- data/lib/appfuel/domain/dsl.rb +247 -0
- data/lib/appfuel/domain/entity.rb +242 -0
- data/lib/appfuel/domain/entity_collection.rb +87 -0
- data/lib/appfuel/domain/expr.rb +127 -0
- data/lib/appfuel/domain/value_object.rb +7 -0
- data/lib/appfuel/errors.rb +104 -0
- data/lib/appfuel/feature.rb +2 -0
- data/lib/appfuel/feature/action_loader.rb +25 -0
- data/lib/appfuel/feature/initializer.rb +43 -0
- data/lib/appfuel/handler.rb +6 -0
- data/lib/appfuel/handler/action.rb +17 -0
- data/lib/appfuel/handler/base.rb +103 -0
- data/lib/appfuel/handler/command.rb +18 -0
- data/lib/appfuel/handler/inject_dsl.rb +88 -0
- data/lib/appfuel/handler/validator_dsl.rb +256 -0
- data/lib/appfuel/initialize.rb +70 -0
- data/lib/appfuel/initialize/initializer.rb +68 -0
- data/lib/appfuel/msg_request.rb +207 -0
- data/lib/appfuel/predicates.rb +10 -0
- data/lib/appfuel/presenter.rb +18 -0
- data/lib/appfuel/presenter/base.rb +7 -0
- data/lib/appfuel/repository.rb +73 -0
- data/lib/appfuel/repository/base.rb +72 -0
- data/lib/appfuel/repository/initializer.rb +19 -0
- data/lib/appfuel/repository/mapper.rb +203 -0
- data/lib/appfuel/repository/mapping_dsl.rb +210 -0
- data/lib/appfuel/repository/mapping_entry.rb +76 -0
- data/lib/appfuel/repository/mapping_registry.rb +121 -0
- data/lib/appfuel/repository_runner.rb +60 -0
- data/lib/appfuel/request.rb +53 -0
- data/lib/appfuel/response.rb +96 -0
- data/lib/appfuel/response_handler.rb +79 -0
- data/lib/appfuel/root_module.rb +31 -0
- data/lib/appfuel/run_error.rb +9 -0
- data/lib/appfuel/storage.rb +3 -0
- data/lib/appfuel/storage/db.rb +4 -0
- data/lib/appfuel/storage/db/active_record_model.rb +42 -0
- data/lib/appfuel/storage/db/mapper.rb +213 -0
- data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
- data/lib/appfuel/storage/db/migration_runner.rb +15 -0
- data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
- data/lib/appfuel/storage/db/repository.rb +231 -0
- data/lib/appfuel/storage/db/repository_query.rb +13 -0
- data/lib/appfuel/storage/file.rb +1 -0
- data/lib/appfuel/storage/file/base.rb +32 -0
- data/lib/appfuel/storage/memory.rb +2 -0
- data/lib/appfuel/storage/memory/mapper.rb +30 -0
- data/lib/appfuel/storage/memory/repository.rb +37 -0
- data/lib/appfuel/types.rb +53 -0
- data/lib/appfuel/validation.rb +80 -0
- data/lib/appfuel/validation/validator.rb +59 -0
- data/lib/appfuel/validation/validator_pipe.rb +47 -0
- data/lib/appfuel/version.rb +3 -0
- metadata +335 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'configuration/file_loader'
|
2
|
+
require_relative 'configuration/search'
|
3
|
+
require_relative 'configuration/populate'
|
4
|
+
require_relative 'configuration/definition_dsl'
|
5
|
+
|
6
|
+
module Appfuel
|
7
|
+
module Configuration
|
8
|
+
def self.define(key, &block)
|
9
|
+
definition = DefinitionDsl.new(key)
|
10
|
+
definition.instance_eval(&block)
|
11
|
+
definition
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Configuration
|
3
|
+
# A configuration definition holds the methods that are exposed in
|
4
|
+
# Config dsl. This definition allows you to define a given configuration
|
5
|
+
# as it would exist in a hash. The dsl collects information like where
|
6
|
+
# the file that holds the config data is stored, validation for that
|
7
|
+
# data and default values. Just like hashes can have nested hashes
|
8
|
+
# you can have nested definitions using the "define" method
|
9
|
+
#
|
10
|
+
# NOTE: currently we only support yaml config files
|
11
|
+
#
|
12
|
+
# @example of dsl usage
|
13
|
+
#
|
14
|
+
# Appfuel::Configuration.define :foo do
|
15
|
+
# file /etc/startplus/offers.yml
|
16
|
+
# defaults bar: 'bif',
|
17
|
+
# baz: 'biz'
|
18
|
+
#
|
19
|
+
# env FOO_BAR: :bar,
|
20
|
+
# FOO_BAZ: :baz
|
21
|
+
#
|
22
|
+
# unsafe :some_key, :other_key
|
23
|
+
#
|
24
|
+
# validator {
|
25
|
+
# required(:name).filled
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# define :bam do
|
29
|
+
# defaults bat: 'hit',
|
30
|
+
# rat: 'cheese'
|
31
|
+
#
|
32
|
+
# validator {
|
33
|
+
# required(:cheese_type).filled
|
34
|
+
# }
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Results in something like this
|
39
|
+
#
|
40
|
+
# hash = {
|
41
|
+
# foo: {
|
42
|
+
# bar: 'bif',
|
43
|
+
# baz: 'baz',
|
44
|
+
# name: <user supplied>,
|
45
|
+
# bam: {
|
46
|
+
# bat: 'hit',
|
47
|
+
# rat: 'cheese',
|
48
|
+
# cheese_type: <user supplied>
|
49
|
+
# }
|
50
|
+
# }
|
51
|
+
# }
|
52
|
+
#
|
53
|
+
class DefinitionDsl
|
54
|
+
include FileLoader
|
55
|
+
include Search
|
56
|
+
include Populate
|
57
|
+
attr_reader :key
|
58
|
+
|
59
|
+
# A definition must be created with a key that will be used in the
|
60
|
+
# resulting configuration hash that is built
|
61
|
+
#
|
62
|
+
# @param key Symbol|String key used config hash
|
63
|
+
# @return Definition
|
64
|
+
def initialize(key)
|
65
|
+
@key = key
|
66
|
+
@defaults = {}
|
67
|
+
@file = []
|
68
|
+
@validator = nil
|
69
|
+
@children = {}
|
70
|
+
@env = {}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Dsl command used to set the file path. When used without params
|
74
|
+
# it returns the file path set.
|
75
|
+
#
|
76
|
+
# @param path String
|
77
|
+
# @return String | nil
|
78
|
+
def file(path = nil)
|
79
|
+
return @file if path.nil?
|
80
|
+
path = [path] if path.is_a?(String)
|
81
|
+
|
82
|
+
unless path.is_a?(Array)
|
83
|
+
fail "file path must be a String or Array of Strings"
|
84
|
+
end
|
85
|
+
@file = path
|
86
|
+
end
|
87
|
+
|
88
|
+
def file?
|
89
|
+
!@file.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Dsl used when you expected to manually pass in the configuration data
|
93
|
+
# and ignore the configuration in the file
|
94
|
+
#
|
95
|
+
# @return nil
|
96
|
+
def delete_file
|
97
|
+
@file = []
|
98
|
+
end
|
99
|
+
|
100
|
+
# Dsl command used to set default values. When used without params
|
101
|
+
# it returns the full default hash
|
102
|
+
#
|
103
|
+
# @param settings Hash
|
104
|
+
# @return Hash
|
105
|
+
def defaults(settings = nil)
|
106
|
+
return @defaults if settings.nil?
|
107
|
+
unless settings.is_a?(Hash)
|
108
|
+
fail ArgumentError, 'defaults must be a hash'
|
109
|
+
end
|
110
|
+
|
111
|
+
@defaults = settings
|
112
|
+
end
|
113
|
+
|
114
|
+
# Dsl command used to define what env variables will me mapped to config
|
115
|
+
# keys
|
116
|
+
#
|
117
|
+
# @param settings Hash
|
118
|
+
# @option <key>=><value> The key is the env variable and the value is
|
119
|
+
# the config key it maps to
|
120
|
+
# @return Hash
|
121
|
+
def env(settings = nil)
|
122
|
+
return @env if settings.nil?
|
123
|
+
unless settings.is_a?(Hash)
|
124
|
+
fail ArgumentError, 'config env settings must be a hash'
|
125
|
+
end
|
126
|
+
|
127
|
+
@env = settings
|
128
|
+
end
|
129
|
+
|
130
|
+
# Dsl to assign validator. When no params are given then it returns
|
131
|
+
# the assigned validator. We use the validation library dry-validation
|
132
|
+
# http://dry-rb.org/gems/dry-validation/. We will consider any object
|
133
|
+
# that implements `call` method a validator.
|
134
|
+
#
|
135
|
+
# @params validator Dry::Validation::Schema
|
136
|
+
# @return validator
|
137
|
+
def validator(&block)
|
138
|
+
return @validator unless block_given?
|
139
|
+
|
140
|
+
@validator = Dry::Validation.Schema(&block)
|
141
|
+
end
|
142
|
+
|
143
|
+
def validator?
|
144
|
+
!@validator.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
# Dsl to add a configuration definition as a child of another
|
148
|
+
# definition
|
149
|
+
#
|
150
|
+
# @param key Symbol
|
151
|
+
# @return Details
|
152
|
+
def define(key, &block)
|
153
|
+
definition = self.class.new(key)
|
154
|
+
definition.instance_eval(&block)
|
155
|
+
self << definition
|
156
|
+
end
|
157
|
+
|
158
|
+
def delete(name)
|
159
|
+
@children.delete(name)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Append a definition to this definition's children
|
163
|
+
#
|
164
|
+
# @param definitions Array | Definition
|
165
|
+
def <<(definitions)
|
166
|
+
list = definitions.is_a?(Array) ? definitions : [definitions]
|
167
|
+
list.each {|item| children[item.key] = item}
|
168
|
+
end
|
169
|
+
|
170
|
+
protected
|
171
|
+
attr_accessor :children
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Configuration
|
3
|
+
# Handle loading files and parsing them correctly based on their type.
|
4
|
+
# The file loader used for loading configuration data into a definition
|
5
|
+
module FileLoader
|
6
|
+
attr_writer :file_module, :json_module, :yaml_module
|
7
|
+
|
8
|
+
def file_module
|
9
|
+
@file_module ||= ::File
|
10
|
+
end
|
11
|
+
|
12
|
+
def json_module
|
13
|
+
@json_module || ::JSON
|
14
|
+
end
|
15
|
+
|
16
|
+
def yaml_module
|
17
|
+
@yaml_module ||= YAML
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param path [String]
|
21
|
+
# @return [Hash]
|
22
|
+
def parse_json(path)
|
23
|
+
file = file_module.read(path)
|
24
|
+
json_module.parse(file)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param path [String]
|
28
|
+
# @return [Hash]
|
29
|
+
def parse_yaml(path)
|
30
|
+
yaml_module.load_file(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Load file will search through a configuration's definition file
|
34
|
+
# paths and use the first on that exists. It parse it based on
|
35
|
+
# the file type.
|
36
|
+
#
|
37
|
+
# @raises [RuntimeException] when no files are found
|
38
|
+
#
|
39
|
+
# @param definition [DefinitionDsl]
|
40
|
+
# @return [Hash]
|
41
|
+
def load_file(definition)
|
42
|
+
paths = definition.file
|
43
|
+
key = definition.key
|
44
|
+
|
45
|
+
paths.each do |path|
|
46
|
+
ext = file_module.extname(path).strip.downcase[1..-1]
|
47
|
+
parse_method = "parse_#{ext}"
|
48
|
+
unless respond_to?(parse_method)
|
49
|
+
fail "extension (#{ext}), for (#{key}: #{path}) is not valid, " +
|
50
|
+
"only yaml and json are supported"
|
51
|
+
end
|
52
|
+
|
53
|
+
return public_send(parse_method, path) if file_module.exists?(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
list = paths.join(',')
|
57
|
+
fail "none of :#{key} config files exist at (#{list})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Configuration
|
3
|
+
module Populate
|
4
|
+
# This converts a definition into a hash of configuation values. It does
|
5
|
+
# this using the following steps
|
6
|
+
#
|
7
|
+
# 1. load config data from a yaml or json file if a file is defined
|
8
|
+
# 2. populate all children
|
9
|
+
# 3. merge defaults into config data that has been given or resolved
|
10
|
+
# from the config file
|
11
|
+
# 4. merge override data into the results from step 3
|
12
|
+
# 5. run validation and assign clean data to config with the key
|
13
|
+
#
|
14
|
+
# @throws RuntimeError when validation fails
|
15
|
+
# @param data Hash holds overrides and config source data
|
16
|
+
# @return Hash
|
17
|
+
def populate(data = {})
|
18
|
+
overrides = data[:overrides] || {}
|
19
|
+
config = data[:config] || {}
|
20
|
+
env_data = data[:env] || ENV
|
21
|
+
|
22
|
+
if overrides.key?(:config_file) && !overrides[:config_file].nil?
|
23
|
+
file overrides[:config_file]
|
24
|
+
end
|
25
|
+
|
26
|
+
if file?
|
27
|
+
config = load_file(self)
|
28
|
+
config = config[key]
|
29
|
+
end
|
30
|
+
|
31
|
+
config ||= {}
|
32
|
+
|
33
|
+
config = defaults.deep_merge(config)
|
34
|
+
config = config.deep_merge(load_env(env_data, self))
|
35
|
+
config = config.deep_merge(overrides || {})
|
36
|
+
|
37
|
+
populate_children(children, config, env_data) unless children.empty?
|
38
|
+
|
39
|
+
handle_validation(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @param definition [DefinitionDsl]
|
44
|
+
# @return [Hash]
|
45
|
+
def load_env(env_data, definition)
|
46
|
+
config = {}
|
47
|
+
definition.env.each do |env_key, config_key|
|
48
|
+
env_key = env_key.to_s
|
49
|
+
next unless env_data.key?(env_key)
|
50
|
+
config[config_key] = env_data[env_key]
|
51
|
+
end
|
52
|
+
config
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def populate_children(child_hash, data, env_data = {})
|
58
|
+
child_hash.each do |(def_key, definition)|
|
59
|
+
|
60
|
+
data[def_key] ||= {}
|
61
|
+
data[def_key] = load_file(definition) if definition.file?
|
62
|
+
data[def_key] = definition.defaults.deep_merge(data[def_key])
|
63
|
+
data[def_key] = data[def_key].deep_merge(load_env(env_data, definition))
|
64
|
+
unless definition.children.empty?
|
65
|
+
populate_children(definition.children, data[def_key], env_data)
|
66
|
+
end
|
67
|
+
|
68
|
+
data[def_key] = definition.handle_validation(data[def_key])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_validation(data)
|
73
|
+
return data unless validator?
|
74
|
+
|
75
|
+
result = validator.call(data)
|
76
|
+
if result.failure?
|
77
|
+
msg = validation_error_message(result.errors(full: true))
|
78
|
+
fail msg
|
79
|
+
end
|
80
|
+
result.to_h
|
81
|
+
end
|
82
|
+
|
83
|
+
def validation_error_message(errors)
|
84
|
+
msg = ''
|
85
|
+
errors.each do |(error_key, values)|
|
86
|
+
if values.is_a?(Hash)
|
87
|
+
values = values.values.uniqu
|
88
|
+
end
|
89
|
+
msg << "[#{key}] #{error_key}: #{values.join("\n")}\n"
|
90
|
+
end
|
91
|
+
msg
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Configuration
|
3
|
+
module Search
|
4
|
+
# Allow you to access child definitions as if it were a hash.
|
5
|
+
# If you add a space separated list of names this will traverse
|
6
|
+
# the child hierarchy and return the last name in the list
|
7
|
+
#
|
8
|
+
# @param name String name or names to search
|
9
|
+
# @return Definition | nil
|
10
|
+
def [](name)
|
11
|
+
find @children, name.to_s.split(" ")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Allows you to search child definitions using an array of names
|
15
|
+
# instead of a space separated string
|
16
|
+
#
|
17
|
+
# @param names Array of strings
|
18
|
+
# @return Definition | nil
|
19
|
+
def search(*names)
|
20
|
+
return nil if names.empty?
|
21
|
+
find children, names
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Recursively locate a child definition in the hierarchy
|
27
|
+
#
|
28
|
+
# @param child_list Hash
|
29
|
+
# @param terms Array of definition keys
|
30
|
+
def find(child_list, terms)
|
31
|
+
while term = terms.shift
|
32
|
+
child_list.each do |(definition_key, definition)|
|
33
|
+
next unless definition_key == term
|
34
|
+
result = if terms.empty?
|
35
|
+
definition
|
36
|
+
else
|
37
|
+
find(definition.children, terms)
|
38
|
+
end
|
39
|
+
return result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Appfuel
|
2
|
+
class DbModel < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
extend Appfuel::Application::ContainerKey
|
5
|
+
extend Appfuel::Application::ContainerClassRegistration
|
6
|
+
|
7
|
+
def self.inherited(klass)
|
8
|
+
super
|
9
|
+
register_container_class(klass)
|
10
|
+
end
|
11
|
+
|
12
|
+
def entity_attributes
|
13
|
+
attributes.symbolize_keys.select {|_,value| !value.nil?}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require_relative "domain/domain_name_parser"
|
2
|
+
require_relative "domain/dsl"
|
3
|
+
require_relative "domain/expr"
|
4
|
+
require_relative "domain/entity"
|
5
|
+
require_relative "domain/value_object"
|
6
|
+
require_relative "domain/entity_collection"
|
7
|
+
require_relative "domain/criteria"
|
@@ -0,0 +1,436 @@
|
|
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
|
+
class Criteria
|
10
|
+
include DomainNameParser
|
11
|
+
|
12
|
+
DEFAULT_PAGE = 1
|
13
|
+
DEFAULT_PER_PAGE = 20
|
14
|
+
|
15
|
+
attr_reader :domain, :domain_name, :feature, :repo_name, :exprs, :order,
|
16
|
+
:exists, :exec, :all
|
17
|
+
|
18
|
+
# Parse out the domain into feature, domain, determine the name of the
|
19
|
+
# repo this criteria is for and initailize basic settings.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# SpCore::Domain::Criteria('foo', single: true)
|
23
|
+
# Types.Criteria('foo.bar', single: true)
|
24
|
+
#
|
25
|
+
# === Options
|
26
|
+
# error_on_empty: will cause the repo to fail when query returns an
|
27
|
+
# an empty dataset. The failure will have the message
|
28
|
+
# with key as domain and text is "<domain> not found"
|
29
|
+
#
|
30
|
+
# single: will cause the repo to return only one, the first,
|
31
|
+
# entity in the dataset
|
32
|
+
#
|
33
|
+
# @param domain [String] fully qualified domain name
|
34
|
+
# @param opts [Hash] options for initializing criteria
|
35
|
+
# @return [Criteria]
|
36
|
+
def initialize(domain, opts = {})
|
37
|
+
@feature, @domain, @domain_name = parse_domain_name(domain)
|
38
|
+
@exists = nil
|
39
|
+
@exprs = []
|
40
|
+
@order = []
|
41
|
+
@limit = nil
|
42
|
+
@exec = nil
|
43
|
+
@all = false
|
44
|
+
@first = false
|
45
|
+
@last = false
|
46
|
+
@params = {}
|
47
|
+
@page = DEFAULT_PAGE
|
48
|
+
@per_page = DEFAULT_PER_PAGE
|
49
|
+
@disable_pagination = opts[:disable_pagination] == true
|
50
|
+
@repo_name = "#{(opts[:repo] || @domain).classify}Repository"
|
51
|
+
|
52
|
+
empty_dataset_is_valid!
|
53
|
+
if opts[:error_on_empty] == true
|
54
|
+
error_on_empty_dataset!
|
55
|
+
end
|
56
|
+
|
57
|
+
# default is to expect a collection for this critria unless you declare
|
58
|
+
# you want a single
|
59
|
+
collection
|
60
|
+
public_send(:first) if opts[:single] == true || opts[:first] == true
|
61
|
+
public_send(:last) if opts[:last] == true
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add param to the instantiated criteria
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# criteria.add_param('foo', 100)
|
68
|
+
#
|
69
|
+
# @param key [Symbol, String] The key name where we want to keep the value
|
70
|
+
# @param value [String, Integer] The value that belongs to the key param
|
71
|
+
# @return [String, Integer] The saved value
|
72
|
+
def add_param(key, value)
|
73
|
+
fail 'key should not be nil' if key.nil?
|
74
|
+
|
75
|
+
@params[key.to_sym] = value
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param key [String, Symbol]
|
79
|
+
# @return [String, Integer, Boolean] the found value
|
80
|
+
def param(key)
|
81
|
+
@params[key.to_sym]
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param key [String, Symbol]
|
85
|
+
# @return [Boolean]
|
86
|
+
def param?(key)
|
87
|
+
@params.key?(key.to_sym)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Boolean] if the @params variable has values
|
91
|
+
def params?
|
92
|
+
!@params.empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Remove operators and keep key filters. It returns a cleaned
|
96
|
+
# list of filters.
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# criteria.filter([
|
100
|
+
# {projects.offer.id: 6},
|
101
|
+
# {last_name: 'SirFooish', op: 'like'},
|
102
|
+
# {first_name: Bob, op: 'like', or: false}
|
103
|
+
# ])
|
104
|
+
#
|
105
|
+
# @raise [RuntimeError] when the attribute is not an array
|
106
|
+
# @raise [RuntimeError] when the filter is not a Hash
|
107
|
+
#
|
108
|
+
# @param list [Array<Hash>] The list of filters to implement in criteria
|
109
|
+
# @return [Array<Hash>, nil] List of filters with values or nil in case of array empty
|
110
|
+
def filter(list)
|
111
|
+
fail 'the attribute must be an Array' unless list.is_a? Array
|
112
|
+
|
113
|
+
list.each do |item|
|
114
|
+
fail 'filters must be a Hash' unless item.is_a? Hash
|
115
|
+
|
116
|
+
operator = extract_relational_operator(item)
|
117
|
+
relational_or = extract_relational_condition(item)
|
118
|
+
key, value = item.first
|
119
|
+
|
120
|
+
where(key, operator => value, or: relational_or)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def where?
|
125
|
+
!exprs.empty? || all?
|
126
|
+
end
|
127
|
+
|
128
|
+
# Adds an expression to the list that will be joined to
|
129
|
+
# the next expression with an `:and` operator
|
130
|
+
#
|
131
|
+
# @param domain_attr [String]
|
132
|
+
# @param data [Hash]
|
133
|
+
# @option <operator> the key is the operator like :eq and value is value
|
134
|
+
# @option :or with a value of true will join this expression with the
|
135
|
+
# previous expression using a relation or. relation and
|
136
|
+
# is by default
|
137
|
+
#
|
138
|
+
# @return [Criteria]
|
139
|
+
def where(domain_attr, data)
|
140
|
+
domain_attr = domain_attr.to_s
|
141
|
+
|
142
|
+
relational_op = :and
|
143
|
+
if data.key?(:or)
|
144
|
+
value = data.delete(:or)
|
145
|
+
relational_op = :or if value == true
|
146
|
+
end
|
147
|
+
|
148
|
+
domain_entity = domain_name
|
149
|
+
if domain_attr.count('.') == 2
|
150
|
+
domain_entity, domain_attr = parse_domain_attr(domain_attr)
|
151
|
+
end
|
152
|
+
|
153
|
+
expr = {
|
154
|
+
expr: create_expr(domain_entity, domain_attr, data),
|
155
|
+
relational_op: relational_op
|
156
|
+
}
|
157
|
+
exprs << expr
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
alias_method :and, :where
|
162
|
+
|
163
|
+
# Adds an expression to the list that will be joined to
|
164
|
+
# the next expression with an `:or` operator
|
165
|
+
#
|
166
|
+
# @param attr [String]
|
167
|
+
# @param value [Hash]
|
168
|
+
# @return [Criteria]
|
169
|
+
def or(domain_attr, data)
|
170
|
+
data[:or] = true
|
171
|
+
where(domain_attr, data)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Adds an expression to order list.
|
175
|
+
#
|
176
|
+
# @param list [list]
|
177
|
+
# @return [Criteria]
|
178
|
+
def order_by(dict, order_dir = 'ASC')
|
179
|
+
if dict.is_a?(String) || dict.is_a?(Symbol)
|
180
|
+
dict = {dict.to_s => order_dir}
|
181
|
+
end
|
182
|
+
|
183
|
+
dict.each do |domain_attr, dir|
|
184
|
+
domain_entity = domain_name
|
185
|
+
|
186
|
+
if domain_attr.count('.') == 2
|
187
|
+
domain_entity, domain_attr = parse_domain_attr(domain_attr)
|
188
|
+
end
|
189
|
+
|
190
|
+
@order << create_expr(domain_entity, domain_attr, eq: dir.to_s.upcase)
|
191
|
+
end
|
192
|
+
|
193
|
+
self
|
194
|
+
end
|
195
|
+
|
196
|
+
def order?
|
197
|
+
!@order.empty?
|
198
|
+
end
|
199
|
+
|
200
|
+
def limit?
|
201
|
+
!@limit.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
# @param limit [Integer]
|
205
|
+
# @return [Criteria]
|
206
|
+
def limit(value = nil)
|
207
|
+
return @limit if value.nil?
|
208
|
+
|
209
|
+
value = Integer(value)
|
210
|
+
fail "limit must be an integer gt 0" unless value > 0
|
211
|
+
@limit = value
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
# @param page [Integer]
|
216
|
+
# @return [Criteria]
|
217
|
+
def page(value = nil)
|
218
|
+
return @page if value.nil?
|
219
|
+
|
220
|
+
@page = Integer(value)
|
221
|
+
self
|
222
|
+
end
|
223
|
+
|
224
|
+
# @param per_page [Integer]
|
225
|
+
# @return [Criteria]
|
226
|
+
def per_page(value=nil)
|
227
|
+
return @per_page if value.nil?
|
228
|
+
|
229
|
+
@per_page = Integer(value)
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
# The repo uses this to determine what kind of dataset to return
|
234
|
+
#
|
235
|
+
# @return [Boolean]
|
236
|
+
def single?
|
237
|
+
first? || last?
|
238
|
+
end
|
239
|
+
|
240
|
+
# Tell the repo to only return a single entity for this criteria
|
241
|
+
#
|
242
|
+
# @return [Criteria]
|
243
|
+
def single
|
244
|
+
first
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
# Set false @first and @last instance variables
|
249
|
+
def clear_single
|
250
|
+
clear_first
|
251
|
+
clear_last
|
252
|
+
end
|
253
|
+
|
254
|
+
def first
|
255
|
+
@first = true
|
256
|
+
clear_last
|
257
|
+
self
|
258
|
+
end
|
259
|
+
|
260
|
+
def first?
|
261
|
+
@first
|
262
|
+
end
|
263
|
+
|
264
|
+
def clear_first
|
265
|
+
@first = false
|
266
|
+
end
|
267
|
+
|
268
|
+
def last?
|
269
|
+
@last
|
270
|
+
end
|
271
|
+
|
272
|
+
def last
|
273
|
+
clear_first
|
274
|
+
@last = true
|
275
|
+
self
|
276
|
+
end
|
277
|
+
|
278
|
+
def clear_last
|
279
|
+
@last = false
|
280
|
+
end
|
281
|
+
|
282
|
+
def disable_pagination?
|
283
|
+
@disable_pagination
|
284
|
+
end
|
285
|
+
|
286
|
+
def disable_pagination
|
287
|
+
@disable_pagination = true
|
288
|
+
end
|
289
|
+
|
290
|
+
# Tell the repo to return a collection for this criteria. This is the
|
291
|
+
# default setting
|
292
|
+
#
|
293
|
+
# @return Criteria
|
294
|
+
def collection
|
295
|
+
clear_single
|
296
|
+
self
|
297
|
+
end
|
298
|
+
|
299
|
+
def all?
|
300
|
+
@all
|
301
|
+
end
|
302
|
+
|
303
|
+
# Tell the repo to return all records for this criteria. It is import
|
304
|
+
# to understand that for database queries you are calling all on the
|
305
|
+
# map for the specified domain.
|
306
|
+
#
|
307
|
+
# @example
|
308
|
+
# Criteria.new('projects.offer').all
|
309
|
+
#
|
310
|
+
# UserRepository has a mapper with a map for 'offer' in this case
|
311
|
+
# all will be called on the db class for this map.
|
312
|
+
#
|
313
|
+
# @return [Criteria]
|
314
|
+
def all
|
315
|
+
@all = true
|
316
|
+
collection
|
317
|
+
self
|
318
|
+
end
|
319
|
+
|
320
|
+
# Used to determin if this criteria belongs to a feature
|
321
|
+
#
|
322
|
+
# @return [Boolean]
|
323
|
+
def feature?
|
324
|
+
!@feature.nil?
|
325
|
+
end
|
326
|
+
|
327
|
+
# Used to determin if this criteria belongs to a global domain
|
328
|
+
#
|
329
|
+
# @return [Boolean]
|
330
|
+
def global_domain?
|
331
|
+
!feature?
|
332
|
+
end
|
333
|
+
|
334
|
+
# Determines if a domain exists in this repo
|
335
|
+
#
|
336
|
+
# @param attr [String]
|
337
|
+
# @param value [Mixed]
|
338
|
+
# @return [Criteria]
|
339
|
+
def exists(domain_attr, value)
|
340
|
+
domain_attr = domain_attr.to_s
|
341
|
+
domain_entity = domain_name
|
342
|
+
if domain_attr.count('.') == 3
|
343
|
+
domain_entity, domain_attr = parse_domain_attr(domain_attr)
|
344
|
+
end
|
345
|
+
@exists = create_expr(domain_entity, domain_attr, eq: value)
|
346
|
+
self
|
347
|
+
end
|
348
|
+
|
349
|
+
# @return [DbEntityExpr]
|
350
|
+
def exists_expr
|
351
|
+
@exists
|
352
|
+
end
|
353
|
+
|
354
|
+
# exec is used to indicate we want a custom method on the repo
|
355
|
+
# to used with this criteria
|
356
|
+
#
|
357
|
+
# @param name [String]
|
358
|
+
# @return [String, Criteria] when used as a dsl it returns the criteria
|
359
|
+
def exec(name = nil)
|
360
|
+
return @exec if name.nil?
|
361
|
+
|
362
|
+
@exec = name.to_sym
|
363
|
+
self
|
364
|
+
end
|
365
|
+
|
366
|
+
def exec?
|
367
|
+
!@exec.nil?
|
368
|
+
end
|
369
|
+
|
370
|
+
# @yield expression and operator
|
371
|
+
# @return [Enumerator] when no block is given
|
372
|
+
def each
|
373
|
+
return exprs.each unless block_given?
|
374
|
+
|
375
|
+
exprs.each do |expr|
|
376
|
+
yield expr[:expr], expr[:relational_op]
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def error_on_empty_dataset?
|
381
|
+
@error_on_empty
|
382
|
+
end
|
383
|
+
|
384
|
+
# Tells the repo to return an error when entity is not found
|
385
|
+
#
|
386
|
+
# @return Criteria
|
387
|
+
def error_on_empty_dataset!
|
388
|
+
@error_on_empty = true
|
389
|
+
self
|
390
|
+
end
|
391
|
+
|
392
|
+
# Tells the repo to return and empty collection, or nil if single is
|
393
|
+
# invoked, if the entity is not found
|
394
|
+
#
|
395
|
+
# @return Criteria
|
396
|
+
def empty_dataset_is_valid!
|
397
|
+
@error_on_empty = false
|
398
|
+
self
|
399
|
+
end
|
400
|
+
|
401
|
+
private
|
402
|
+
def parse_domain_name(name)
|
403
|
+
if !name.is_a?(String) && !name.respond_to?(:domain_name)
|
404
|
+
fail "domain name must be a string or implement method :domain_name"
|
405
|
+
end
|
406
|
+
|
407
|
+
name = name.domain_name if name.respond_to?(:domain_name)
|
408
|
+
feature, domain = name.split('.', 2)
|
409
|
+
if domain.nil?
|
410
|
+
domain = feature
|
411
|
+
feature = nil
|
412
|
+
end
|
413
|
+
[feature, domain, name]
|
414
|
+
end
|
415
|
+
|
416
|
+
def create_expr(domain_name, domain_attr, value)
|
417
|
+
Expr.new(domain_name, domain_attr, value)
|
418
|
+
end
|
419
|
+
|
420
|
+
def extract_relational_condition(filter_item)
|
421
|
+
relational_or = (filter_item.delete(:or) == true) if filter_item.key?(:or)
|
422
|
+
relational_or
|
423
|
+
end
|
424
|
+
|
425
|
+
def extract_relational_operator(filter_item)
|
426
|
+
operator = "eq"
|
427
|
+
operator = filter_item.delete(:op) if filter_item.key?(:op)
|
428
|
+
if filter_item[:insensitive]
|
429
|
+
operator = "ilike"
|
430
|
+
filter_item.delete(:insensitive)
|
431
|
+
end
|
432
|
+
operator
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|