appfuel 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +78 -1
- data/appfuel.gemspec +1 -0
- data/docs/images/appfuel_basic_flow.png +0 -0
- data/lib/appfuel/application/app_container.rb +24 -1
- data/lib/appfuel/application/container_class_registration.rb +29 -4
- data/lib/appfuel/application/root.rb +1 -0
- data/lib/appfuel/application.rb +0 -1
- data/lib/appfuel/domain/base_criteria.rb +171 -0
- data/lib/appfuel/domain/criteria_builder.rb +248 -0
- data/lib/appfuel/domain/criteria_settings.rb +156 -0
- data/lib/appfuel/domain/dsl.rb +5 -1
- data/lib/appfuel/domain/entity.rb +1 -2
- data/lib/appfuel/domain/exists_criteria.rb +57 -0
- data/lib/appfuel/domain/expr.rb +66 -97
- data/lib/appfuel/domain/expr_conjunction.rb +27 -0
- data/lib/appfuel/domain/expr_parser.rb +199 -0
- data/lib/appfuel/domain/expr_transform.rb +68 -0
- data/lib/appfuel/domain/search_criteria.rb +137 -0
- data/lib/appfuel/domain.rb +6 -1
- data/lib/appfuel/feature/initializer.rb +5 -0
- data/lib/appfuel/handler/action.rb +3 -0
- data/lib/appfuel/handler/base.rb +11 -1
- data/lib/appfuel/handler/command.rb +4 -0
- data/lib/appfuel/repository/base.rb +16 -2
- data/lib/appfuel/repository/mapper.rb +41 -7
- data/lib/appfuel/repository/mapping_dsl.rb +4 -4
- data/lib/appfuel/repository/mapping_entry.rb +2 -2
- data/lib/appfuel/repository.rb +0 -1
- data/lib/appfuel/storage/db/active_record_model.rb +32 -28
- data/lib/appfuel/storage/db/mapper.rb +38 -125
- data/lib/appfuel/storage/db/repository.rb +6 -10
- data/lib/appfuel/storage/memory/repository.rb +4 -0
- data/lib/appfuel/types.rb +0 -1
- data/lib/appfuel/version.rb +1 -1
- data/lib/appfuel.rb +6 -10
- metadata +26 -7
- data/lib/appfuel/application/container_key.rb +0 -201
- data/lib/appfuel/application/qualify_container_key.rb +0 -76
- data/lib/appfuel/db_model.rb +0 -16
- data/lib/appfuel/domain/criteria.rb +0 -436
- data/lib/appfuel/repository/mapping_registry.rb +0 -121
@@ -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
|