appfuel 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +25 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +19 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +9 -0
  9. data/README.md +38 -0
  10. data/Rakefile +6 -0
  11. data/appfuel.gemspec +42 -0
  12. data/bin/console +7 -0
  13. data/bin/setup +8 -0
  14. data/lib/appfuel.rb +210 -0
  15. data/lib/appfuel/application.rb +4 -0
  16. data/lib/appfuel/application/app_container.rb +223 -0
  17. data/lib/appfuel/application/container_class_registration.rb +22 -0
  18. data/lib/appfuel/application/container_key.rb +201 -0
  19. data/lib/appfuel/application/qualify_container_key.rb +76 -0
  20. data/lib/appfuel/application/root.rb +140 -0
  21. data/lib/appfuel/cli_msg_request.rb +19 -0
  22. data/lib/appfuel/configuration.rb +14 -0
  23. data/lib/appfuel/configuration/definition_dsl.rb +175 -0
  24. data/lib/appfuel/configuration/file_loader.rb +61 -0
  25. data/lib/appfuel/configuration/populate.rb +95 -0
  26. data/lib/appfuel/configuration/search.rb +45 -0
  27. data/lib/appfuel/db_model.rb +16 -0
  28. data/lib/appfuel/domain.rb +7 -0
  29. data/lib/appfuel/domain/criteria.rb +436 -0
  30. data/lib/appfuel/domain/domain_name_parser.rb +44 -0
  31. data/lib/appfuel/domain/dsl.rb +247 -0
  32. data/lib/appfuel/domain/entity.rb +242 -0
  33. data/lib/appfuel/domain/entity_collection.rb +87 -0
  34. data/lib/appfuel/domain/expr.rb +127 -0
  35. data/lib/appfuel/domain/value_object.rb +7 -0
  36. data/lib/appfuel/errors.rb +104 -0
  37. data/lib/appfuel/feature.rb +2 -0
  38. data/lib/appfuel/feature/action_loader.rb +25 -0
  39. data/lib/appfuel/feature/initializer.rb +43 -0
  40. data/lib/appfuel/handler.rb +6 -0
  41. data/lib/appfuel/handler/action.rb +17 -0
  42. data/lib/appfuel/handler/base.rb +103 -0
  43. data/lib/appfuel/handler/command.rb +18 -0
  44. data/lib/appfuel/handler/inject_dsl.rb +88 -0
  45. data/lib/appfuel/handler/validator_dsl.rb +256 -0
  46. data/lib/appfuel/initialize.rb +70 -0
  47. data/lib/appfuel/initialize/initializer.rb +68 -0
  48. data/lib/appfuel/msg_request.rb +207 -0
  49. data/lib/appfuel/predicates.rb +10 -0
  50. data/lib/appfuel/presenter.rb +18 -0
  51. data/lib/appfuel/presenter/base.rb +7 -0
  52. data/lib/appfuel/repository.rb +73 -0
  53. data/lib/appfuel/repository/base.rb +72 -0
  54. data/lib/appfuel/repository/initializer.rb +19 -0
  55. data/lib/appfuel/repository/mapper.rb +203 -0
  56. data/lib/appfuel/repository/mapping_dsl.rb +210 -0
  57. data/lib/appfuel/repository/mapping_entry.rb +76 -0
  58. data/lib/appfuel/repository/mapping_registry.rb +121 -0
  59. data/lib/appfuel/repository_runner.rb +60 -0
  60. data/lib/appfuel/request.rb +53 -0
  61. data/lib/appfuel/response.rb +96 -0
  62. data/lib/appfuel/response_handler.rb +79 -0
  63. data/lib/appfuel/root_module.rb +31 -0
  64. data/lib/appfuel/run_error.rb +9 -0
  65. data/lib/appfuel/storage.rb +3 -0
  66. data/lib/appfuel/storage/db.rb +4 -0
  67. data/lib/appfuel/storage/db/active_record_model.rb +42 -0
  68. data/lib/appfuel/storage/db/mapper.rb +213 -0
  69. data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
  70. data/lib/appfuel/storage/db/migration_runner.rb +15 -0
  71. data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
  72. data/lib/appfuel/storage/db/repository.rb +231 -0
  73. data/lib/appfuel/storage/db/repository_query.rb +13 -0
  74. data/lib/appfuel/storage/file.rb +1 -0
  75. data/lib/appfuel/storage/file/base.rb +32 -0
  76. data/lib/appfuel/storage/memory.rb +2 -0
  77. data/lib/appfuel/storage/memory/mapper.rb +30 -0
  78. data/lib/appfuel/storage/memory/repository.rb +37 -0
  79. data/lib/appfuel/types.rb +53 -0
  80. data/lib/appfuel/validation.rb +80 -0
  81. data/lib/appfuel/validation/validator.rb +59 -0
  82. data/lib/appfuel/validation/validator_pipe.rb +47 -0
  83. data/lib/appfuel/version.rb +3 -0
  84. 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