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