appfuel 0.2.3 → 0.2.4
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 +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
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Appfuel
|
4
|
+
module Domain
|
5
|
+
# A PEG (Parser Expression Grammer) transformer for our domain language.
|
6
|
+
#
|
7
|
+
class ExprTransform < Parslet::Transform
|
8
|
+
rule(integer: simple(:n)) { Integer(n) }
|
9
|
+
rule(float: simple(:n)) { Float(n) }
|
10
|
+
rule(boolean: simple(:b)) { b.downcase == 'true' }
|
11
|
+
rule(datetime: simple(:dt)) { Time.parse(dt) }
|
12
|
+
rule(date: simple(:d)) { Date.parse(d) }
|
13
|
+
rule(string: simple(:s)) do
|
14
|
+
s.to_s.gsub(/\\[0tnr]/, "\\0" => "\0",
|
15
|
+
"\\t" => "\t",
|
16
|
+
"\\n" => "\n",
|
17
|
+
"\\r" => "\r")
|
18
|
+
end
|
19
|
+
|
20
|
+
rule(domain_expr: subtree(:expr)) do |dict|
|
21
|
+
expr = dict[:expr]
|
22
|
+
attrs = build_domain_attrs(expr[:domain_attr])
|
23
|
+
op = expr[:op].to_s.strip.downcase
|
24
|
+
value = expr[:value]
|
25
|
+
{domain_expr: Expr.new(attrs, op, value)}
|
26
|
+
end
|
27
|
+
|
28
|
+
rule(and: subtree(:data)) do |dict|
|
29
|
+
{root: build_conjunction('and', dict[:data])}
|
30
|
+
end
|
31
|
+
|
32
|
+
rule(or: subtree(:data)) do |dict|
|
33
|
+
{root: build_conjunction('or', dict[:data])}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.build_conjunction(op, data)
|
37
|
+
left = build_conjunction_node(data[:left])
|
38
|
+
right = build_conjunction_node(data[:right])
|
39
|
+
|
40
|
+
ExprConjunction.new(op, left, right)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.build_conjunction_node(data)
|
44
|
+
if data.key?(:root)
|
45
|
+
node = data[:root]
|
46
|
+
elsif data.key?(:domain_expr)
|
47
|
+
node = data[:domain_expr]
|
48
|
+
elsif data.key?(:and) || data.key?(:or)
|
49
|
+
op = right.key?(:and) ? 'and' : 'or'
|
50
|
+
node = build_conjunction(op, data)
|
51
|
+
end
|
52
|
+
node
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.build_domain_attrs(domain_attr)
|
56
|
+
attr_list = []
|
57
|
+
if domain_attr.is_a?(Hash)
|
58
|
+
attr_list << domain_attr[:attr_label].to_s
|
59
|
+
else
|
60
|
+
domain_attr.each do |label|
|
61
|
+
attr_list << label[:attr_label].to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
attr_list
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,137 @@
|
|
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
|
+
# search: 'foo.bar filter id = 6 and bar = "foo" order id asc limit 6'
|
11
|
+
#
|
12
|
+
# filters: 'id = 6 or id = 8 and id = 9'
|
13
|
+
# filters: [
|
14
|
+
# 'id = 6',
|
15
|
+
# {or: 'id = 8'}
|
16
|
+
# {and: id = 9'}
|
17
|
+
# ]
|
18
|
+
#
|
19
|
+
# order: 'foo.bar.id asc'
|
20
|
+
# order: 'foo.bar.id'
|
21
|
+
# order: [
|
22
|
+
# 'foo.bar.id',
|
23
|
+
# {desc: 'foo.bar.id'},
|
24
|
+
# {asc: 'foo.bar.id'}
|
25
|
+
# ]
|
26
|
+
# limit: 1
|
27
|
+
#
|
28
|
+
# settings:
|
29
|
+
# page: 1
|
30
|
+
# per_page: 2
|
31
|
+
# disable_pagination
|
32
|
+
# first
|
33
|
+
# all
|
34
|
+
# last
|
35
|
+
# error_on_empty
|
36
|
+
# parser
|
37
|
+
# transform
|
38
|
+
#
|
39
|
+
class SearchCriteria < BaseCriteria
|
40
|
+
|
41
|
+
# Parse out the domain into feature, domain, determine the name of the
|
42
|
+
# repo this criteria is for and initailize basic settings.
|
43
|
+
# global.user
|
44
|
+
#
|
45
|
+
# membership.user
|
46
|
+
# foo.id filter name like "foo" order foo.bar.id asc limit 2
|
47
|
+
# foo.id exists foo.id = 5
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# SpCore::Domain::Criteria('foo', single: true)
|
51
|
+
# Types.Criteria('foo.bar', single: true)
|
52
|
+
#
|
53
|
+
# === Options
|
54
|
+
# error_on_empty: will cause the repo to fail when query returns an
|
55
|
+
# an empty dataset. The failure will have the message
|
56
|
+
# with key as domain and text is "<domain> not found"
|
57
|
+
#
|
58
|
+
# single: will cause the repo to return only one, the first,
|
59
|
+
# entity in the dataset
|
60
|
+
#
|
61
|
+
# @param domain [String] fully qualified domain name
|
62
|
+
# @param opts [Hash] options for initializing criteria
|
63
|
+
# @return [Criteria]
|
64
|
+
#
|
65
|
+
# @param domain [String] fully qualified domain name
|
66
|
+
# @param opts [Hash] options for initializing criteria
|
67
|
+
# @return [Criteria]
|
68
|
+
def initialize(domain_name, data = {})
|
69
|
+
super
|
70
|
+
@limit = nil
|
71
|
+
@order = []
|
72
|
+
filter(data[:filter]) if data[:filter]
|
73
|
+
end
|
74
|
+
|
75
|
+
def filter(str, op: 'and')
|
76
|
+
expr = parse_expr(str)
|
77
|
+
return false unless expr
|
78
|
+
|
79
|
+
expr = qualify_expr(expr)
|
80
|
+
if filters?
|
81
|
+
expr = ExprConjunction.new(op, filters, expr)
|
82
|
+
end
|
83
|
+
|
84
|
+
@filters = filter
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def limit(nbr = nil)
|
89
|
+
return @limit if nbr.nil?
|
90
|
+
|
91
|
+
@limit = Integer(nbr)
|
92
|
+
fail "limit must be an integer greater than 0" unless nbr > 0
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
# order first_name asc, last_name
|
97
|
+
# order: 'foo.bar.id asc'
|
98
|
+
# order: 'foo.bar.id'
|
99
|
+
# order: [
|
100
|
+
# 'foo.bar.id',
|
101
|
+
# {desc: 'foo.bar.id'},
|
102
|
+
# {asc: 'foo.bar.id'}
|
103
|
+
# ]
|
104
|
+
#
|
105
|
+
# membership.user.id
|
106
|
+
def order(data)
|
107
|
+
data = [data] if data.is_a?(String)
|
108
|
+
unless data.respond_to?(:each)
|
109
|
+
fail "order must be a string or implement :each"
|
110
|
+
end
|
111
|
+
data.each do |item|
|
112
|
+
item = transform_order_string(item) if item.is_a?(String)
|
113
|
+
|
114
|
+
if !item.is_a?(Hash)
|
115
|
+
fail "order array must be a list of strings or hashes"
|
116
|
+
end
|
117
|
+
|
118
|
+
dir, domain_attr = item.first
|
119
|
+
dir = dir.to_s.downcase
|
120
|
+
unless ['desc', 'asc'].include(dir)
|
121
|
+
fail "order array item must have a hash key of desc or asc"
|
122
|
+
end
|
123
|
+
@order << {dir => domain_attr}
|
124
|
+
end
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def transform_order_string(str)
|
131
|
+
str, dir = item.split(' ')
|
132
|
+
dir = 'asc' if dir.nil?
|
133
|
+
{dir.downcase => str}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/appfuel/domain.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
require_relative "domain/domain_name_parser"
|
2
2
|
require_relative "domain/dsl"
|
3
3
|
require_relative "domain/expr"
|
4
|
+
require_relative "domain/expr_conjunction"
|
4
5
|
require_relative "domain/entity"
|
5
6
|
require_relative "domain/value_object"
|
6
7
|
require_relative "domain/entity_collection"
|
7
|
-
require_relative "domain/
|
8
|
+
require_relative "domain/criteria_settings"
|
9
|
+
require_relative "domain/base_criteria"
|
10
|
+
require_relative "domain/search_criteria"
|
11
|
+
require_relative "domain/exists_criteria"
|
12
|
+
require_relative "domain/expr_parser"
|
@@ -22,6 +22,11 @@ module Appfuel
|
|
22
22
|
require "#{container[:features_path]}/#{name}"
|
23
23
|
end
|
24
24
|
|
25
|
+
container[:auto_register_classes].each do |klass|
|
26
|
+
next unless klass.register?
|
27
|
+
container.register(klass.container_class_path, klass)
|
28
|
+
end
|
29
|
+
|
25
30
|
return false if initialized?(container, feature_key)
|
26
31
|
|
27
32
|
Appfuel.run_initializers(feature_key, container)
|
data/lib/appfuel/handler/base.rb
CHANGED
@@ -14,7 +14,7 @@ module Appfuel
|
|
14
14
|
# @param klass [Class] the handler class that is inheriting this
|
15
15
|
# @return nil
|
16
16
|
def inherited(klass)
|
17
|
-
|
17
|
+
stage_class_for_registration(klass)
|
18
18
|
end
|
19
19
|
|
20
20
|
def response_handler
|
@@ -83,6 +83,16 @@ module Appfuel
|
|
83
83
|
self.class.error(*args)
|
84
84
|
end
|
85
85
|
|
86
|
+
def search(repo, domain, options = {})
|
87
|
+
criteria = build_criteria(domain, options)
|
88
|
+
repo = data[repo]
|
89
|
+
repo.search(criteria)
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_criteria(domain, options)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
86
96
|
def present(name, data, inputs = {})
|
87
97
|
return data if inputs[:raw] == true
|
88
98
|
|
@@ -5,8 +5,13 @@ module Appfuel
|
|
5
5
|
|
6
6
|
class << self
|
7
7
|
attr_writer :mapper
|
8
|
+
|
9
|
+
def container_class_type
|
10
|
+
'repositories'
|
11
|
+
end
|
12
|
+
|
8
13
|
def inherited(klass)
|
9
|
-
|
14
|
+
stage_class_for_registration(klass)
|
10
15
|
end
|
11
16
|
|
12
17
|
def mapper
|
@@ -14,7 +19,7 @@ module Appfuel
|
|
14
19
|
end
|
15
20
|
|
16
21
|
def create_mapper(maps = nil)
|
17
|
-
Mapper.new(maps)
|
22
|
+
Mapper.new(container_root_name, maps)
|
18
23
|
end
|
19
24
|
end
|
20
25
|
|
@@ -22,6 +27,15 @@ module Appfuel
|
|
22
27
|
self.class.mapper
|
23
28
|
end
|
24
29
|
|
30
|
+
def search(criteria)
|
31
|
+
mapper.search(criteria)
|
32
|
+
end
|
33
|
+
|
34
|
+
def exists?(criteria)
|
35
|
+
expr = criteria.fiilters
|
36
|
+
mapper.exists?(expr)
|
37
|
+
end
|
38
|
+
|
25
39
|
def to_storage(entity, exclude = [])
|
26
40
|
mapper.to_storage(entity, exclude)
|
27
41
|
end
|
@@ -118,9 +118,12 @@ module Appfuel
|
|
118
118
|
# @param entity [String] name of the entity
|
119
119
|
# @param attr [String] name of the attribute
|
120
120
|
# @return [Object]
|
121
|
-
def storage_class(
|
122
|
-
entry = find(
|
121
|
+
def storage_class(entity_name, entity_attr, type)
|
122
|
+
entry = find(entity_name, entity_attr)
|
123
|
+
storage_class_from_entry(entry, type)
|
124
|
+
end
|
123
125
|
|
126
|
+
def storage_class_from_entry(entry, type)
|
124
127
|
unless entry.storage?(type)
|
125
128
|
fail "No (#{type}) storage has been mapped"
|
126
129
|
end
|
@@ -128,26 +131,57 @@ module Appfuel
|
|
128
131
|
container_name = entry.container_name
|
129
132
|
unless container_root_name == container_name
|
130
133
|
fail "You can not access a mapping outside of this container " +
|
131
|
-
"(#{container_root_name}, #{container_name})"
|
134
|
+
"(mapper: #{container_root_name}, entry: #{container_name})"
|
132
135
|
end
|
133
|
-
app_container = Appfuel.app_container(entry.
|
136
|
+
app_container = Appfuel.app_container(entry.container_name)
|
134
137
|
key = entry.storage(type)
|
135
138
|
app_container[key]
|
136
139
|
end
|
137
140
|
|
138
|
-
def to_entity_hash(domain_name, data
|
141
|
+
def to_entity_hash(domain_name, data)
|
139
142
|
entity_attrs = {}
|
140
143
|
each_entity_attr(domain_name) do |entry|
|
141
144
|
attr_name = entry.storage_attr
|
142
145
|
domain_attr = entry.domain_attr
|
143
146
|
next unless data.key?(attr_name)
|
144
|
-
|
145
147
|
update_entity_hash(domain_attr, data[attr_name], entity_attrs)
|
146
148
|
end
|
147
149
|
|
148
150
|
entity_attrs
|
149
151
|
end
|
150
152
|
|
153
|
+
# Convert the domain into a hash of storage attributes that represent.
|
154
|
+
# Each storage class has its own hash of mapped attributes. A domain
|
155
|
+
# can have more than one storage class.
|
156
|
+
#
|
157
|
+
# @param domain [Appfuel::Domain::Entity]
|
158
|
+
# @param type [Symbol] type of storage :db, :file, :memory etc...
|
159
|
+
# @param opts [Hash]
|
160
|
+
# @option exclued [Array] list of columns to exclude from mapping
|
161
|
+
#
|
162
|
+
# @return [Hash] each key is a storage class with a hash of column
|
163
|
+
# name/value
|
164
|
+
def to_storage(domain, type, opts = {})
|
165
|
+
unless domain.respond_to?(:domain_name)
|
166
|
+
fail "Domain entity must implement :domain_name"
|
167
|
+
end
|
168
|
+
|
169
|
+
excluded = opts[:exclude] || []
|
170
|
+
data = {}
|
171
|
+
each_entity_attr(domain.domain_name) do |entry|
|
172
|
+
unless entry.storage?(type)
|
173
|
+
"storage type (#{type}) is not support for (#{domain.domain_name})"
|
174
|
+
end
|
175
|
+
storage_attr = entry.storage_attr
|
176
|
+
storage_class = entry.storage(type)
|
177
|
+
next if excluded.include?(storage_attr) || entry.skip?
|
178
|
+
|
179
|
+
data[storage_class] = {} unless data.key?(storage_class)
|
180
|
+
data[storage_class][storage_attr] = entity_value(domain, entry)
|
181
|
+
end
|
182
|
+
data
|
183
|
+
end
|
184
|
+
|
151
185
|
def update_entity_hash(domain_attr, value, hash)
|
152
186
|
if domain_attr.include?('.')
|
153
187
|
hash.deep_merge!(create_entity_hash(domain_attr, value))
|
@@ -159,7 +193,7 @@ module Appfuel
|
|
159
193
|
def entity_value(domain, map_entry)
|
160
194
|
value = resolve_entity_value(domain, map_entry.domain_attr)
|
161
195
|
if map_entry.computed_attr?
|
162
|
-
value = map_entry.
|
196
|
+
value = map_entry.computed_attr(value, domain)
|
163
197
|
end
|
164
198
|
|
165
199
|
value = nil if undefined?(value)
|
@@ -165,13 +165,13 @@ module Appfuel
|
|
165
165
|
end
|
166
166
|
end
|
167
167
|
|
168
|
-
def initialize_file_storage(value
|
168
|
+
def initialize_file_storage(value)
|
169
169
|
key = translate_storage_key(:file, domain_name)
|
170
170
|
case value
|
171
171
|
when true
|
172
172
|
{
|
173
|
-
model: '
|
174
|
-
path: "#{storage_path}/#{key.
|
173
|
+
model: 'file.model',
|
174
|
+
path: "#{storage_path}/#{key.tr('.', '/')}.yml"
|
175
175
|
}
|
176
176
|
end
|
177
177
|
end
|
@@ -195,7 +195,7 @@ module Appfuel
|
|
195
195
|
|
196
196
|
top, *parts = partial_key.split('.')
|
197
197
|
top = "features.#{top}" unless top == 'global'
|
198
|
-
"#{top}
|
198
|
+
"#{top}.#{type}.#{parts.join('.')}"
|
199
199
|
end
|
200
200
|
|
201
201
|
def assign_storage(type, partial_key)
|
@@ -51,10 +51,10 @@ module Appfuel
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def computed_attr?
|
54
|
-
|
54
|
+
!@computed_attr.nil?
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
57
|
+
def computed_attr(value, domain)
|
58
58
|
fail "No lambda assigned to compute value" unless computed_attr?
|
59
59
|
@computed_attr.call(value, domain)
|
60
60
|
end
|
data/lib/appfuel/repository.rb
CHANGED
@@ -2,7 +2,6 @@ require_relative 'repository/base'
|
|
2
2
|
require_relative 'repository/mapping_entry'
|
3
3
|
require_relative 'repository/mapping_dsl'
|
4
4
|
require_relative 'repository/mapper'
|
5
|
-
require_relative 'repository/mapping_registry'
|
6
5
|
require_relative 'repository/initializer'
|
7
6
|
|
8
7
|
module Appfuel
|
@@ -1,41 +1,45 @@
|
|
1
1
|
module Appfuel
|
2
2
|
module Db
|
3
|
+
# ActiveRecord::Base that auto registers itself into the application
|
4
|
+
# container and symbolizes its attributes. This is used by the db
|
5
|
+
# mapper to persist and retreive domains to and from the database.
|
6
|
+
#
|
7
|
+
# NOTE: we are coupling ourselves to active record right now. I have
|
8
|
+
# plans to resolve this but right now its a lower priority. You
|
9
|
+
# can get around this by implementing your own mapper, and db model.
|
10
|
+
#
|
3
11
|
class ActiveRecordModel < ActiveRecord::Base
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
# ChangeOrder::Membership::Domains::Account
|
10
|
-
#
|
11
|
-
# Appfuel.mapping membership.account,
|
12
|
-
# db: account, yaml: account do
|
13
|
-
# map id, account.id
|
14
|
-
# end
|
15
|
-
#
|
16
|
-
# module Membership
|
17
|
-
# module Db
|
18
|
-
#
|
19
|
-
# end
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# global.db.foobar
|
12
|
+
include Appfuel::Application::AppContainer
|
13
|
+
|
14
|
+
self.abstract_class = true
|
15
|
+
|
16
|
+
# Contributes to the construction of a fully qualified container key
|
24
17
|
#
|
25
|
-
#
|
26
|
-
#
|
18
|
+
# @example
|
19
|
+
# global model: global.db.user
|
20
|
+
# feature model: features.membership.db.user
|
27
21
|
#
|
22
|
+
# @return [String]
|
23
|
+
def self.container_class_type
|
24
|
+
'db'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Registers the class inside the application container. The class
|
28
|
+
# being registered as the mixin required for registration.
|
28
29
|
#
|
29
|
-
|
30
|
-
|
30
|
+
# @params klass [Class] class that is inheriting this one
|
31
|
+
# @return results from super
|
31
32
|
def self.inherited(klass)
|
33
|
+
stage_class_for_registration(klass)
|
32
34
|
super
|
33
|
-
register_container_class(klass)
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
# Symbolize active record attributes and remove attributes with
|
38
|
+
# nil values
|
39
|
+
#
|
40
|
+
# @return [Hash]
|
41
|
+
def domain_attrs
|
42
|
+
attributes.symbolize_keys.select {|_k, v| !v.nil? }
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|