graphiti-rb 1.0.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/.yardopts +2 -0
- data/Appraisals +11 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +12 -0
- data/Guardfile +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/Rakefile +15 -0
- data/bin/appraisal +17 -0
- data/bin/console +14 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/gemfiles/rails_4.gemfile +17 -0
- data/gemfiles/rails_5.gemfile +17 -0
- data/graphiti.gemspec +34 -0
- data/lib/generators/jsonapi/resource_generator.rb +169 -0
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
- data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
- data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
- data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
- data/lib/graphiti-rb.rb +1 -0
- data/lib/graphiti.rb +121 -0
- data/lib/graphiti/adapters/abstract.rb +516 -0
- data/lib/graphiti/adapters/active_record.rb +6 -0
- data/lib/graphiti/adapters/active_record/base.rb +249 -0
- data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
- data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/graphiti/adapters/null.rb +236 -0
- data/lib/graphiti/base.rb +70 -0
- data/lib/graphiti/configuration.rb +21 -0
- data/lib/graphiti/context.rb +16 -0
- data/lib/graphiti/deserializer.rb +208 -0
- data/lib/graphiti/errors.rb +309 -0
- data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
- data/lib/graphiti/extensions/extra_attribute.rb +70 -0
- data/lib/graphiti/extensions/temp_id.rb +26 -0
- data/lib/graphiti/filter_operators.rb +25 -0
- data/lib/graphiti/hash_renderer.rb +57 -0
- data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
- data/lib/graphiti/query.rb +251 -0
- data/lib/graphiti/rails.rb +28 -0
- data/lib/graphiti/railtie.rb +74 -0
- data/lib/graphiti/renderer.rb +60 -0
- data/lib/graphiti/resource.rb +110 -0
- data/lib/graphiti/resource/configuration.rb +239 -0
- data/lib/graphiti/resource/dsl.rb +138 -0
- data/lib/graphiti/resource/interface.rb +32 -0
- data/lib/graphiti/resource/polymorphism.rb +68 -0
- data/lib/graphiti/resource/sideloading.rb +102 -0
- data/lib/graphiti/resource_proxy.rb +127 -0
- data/lib/graphiti/responders.rb +19 -0
- data/lib/graphiti/runner.rb +25 -0
- data/lib/graphiti/scope.rb +98 -0
- data/lib/graphiti/scoping/base.rb +99 -0
- data/lib/graphiti/scoping/default_filter.rb +58 -0
- data/lib/graphiti/scoping/extra_attributes.rb +29 -0
- data/lib/graphiti/scoping/filter.rb +93 -0
- data/lib/graphiti/scoping/filterable.rb +36 -0
- data/lib/graphiti/scoping/paginate.rb +87 -0
- data/lib/graphiti/scoping/sort.rb +64 -0
- data/lib/graphiti/sideload.rb +281 -0
- data/lib/graphiti/sideload/belongs_to.rb +34 -0
- data/lib/graphiti/sideload/has_many.rb +16 -0
- data/lib/graphiti/sideload/has_one.rb +9 -0
- data/lib/graphiti/sideload/many_to_many.rb +24 -0
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/graphiti/stats/dsl.rb +89 -0
- data/lib/graphiti/stats/payload.rb +49 -0
- data/lib/graphiti/types.rb +172 -0
- data/lib/graphiti/util/attribute_check.rb +88 -0
- data/lib/graphiti/util/field_params.rb +16 -0
- data/lib/graphiti/util/hash.rb +51 -0
- data/lib/graphiti/util/hooks.rb +33 -0
- data/lib/graphiti/util/include_params.rb +39 -0
- data/lib/graphiti/util/persistence.rb +219 -0
- data/lib/graphiti/util/relationship_payload.rb +64 -0
- data/lib/graphiti/util/serializer_attributes.rb +97 -0
- data/lib/graphiti/util/sideload.rb +33 -0
- data/lib/graphiti/util/validation_response.rb +78 -0
- data/lib/graphiti/version.rb +3 -0
- metadata +317 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
# Private, tested in resource specs
|
2
|
+
module Graphiti
|
3
|
+
module Util
|
4
|
+
class AttributeCheck
|
5
|
+
attr_reader :resource, :name, :flag, :request, :raise_error
|
6
|
+
|
7
|
+
def self.run(resource, name, flag, request, raise_error)
|
8
|
+
new(resource, name, flag, request, raise_error).run
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(resource, name, flag, request, raise_error)
|
12
|
+
@resource = resource
|
13
|
+
@name = name.to_sym
|
14
|
+
@flag = flag
|
15
|
+
@request = request
|
16
|
+
@raise_error = raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
if attribute?
|
21
|
+
if supported?
|
22
|
+
if guarded?
|
23
|
+
if guard_passes?
|
24
|
+
attribute
|
25
|
+
else
|
26
|
+
maybe_raise(request: true, guard: attribute[flag])
|
27
|
+
end
|
28
|
+
else
|
29
|
+
attribute
|
30
|
+
end
|
31
|
+
else
|
32
|
+
maybe_raise(exists: true)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
maybe_raise(exists: false)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def maybe_raise(opts = {})
|
40
|
+
default = { request: request, exists: true }
|
41
|
+
opts = default.merge(opts)
|
42
|
+
if raise_error?(opts[:exists])
|
43
|
+
raise error_class.new(resource, name, flag, opts)
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def guard_passes?
|
50
|
+
!!resource.send(attribute[flag])
|
51
|
+
end
|
52
|
+
|
53
|
+
def guarded?
|
54
|
+
request? &&
|
55
|
+
attribute[flag].is_a?(Symbol) &&
|
56
|
+
attribute[flag] != :required
|
57
|
+
end
|
58
|
+
|
59
|
+
def error_class
|
60
|
+
Errors::AttributeError
|
61
|
+
end
|
62
|
+
|
63
|
+
def supported?
|
64
|
+
attribute[flag] != false
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute
|
68
|
+
resource.all_attributes[name]
|
69
|
+
end
|
70
|
+
|
71
|
+
def attribute?
|
72
|
+
!!attribute
|
73
|
+
end
|
74
|
+
|
75
|
+
def raise_error?(exists)
|
76
|
+
if raise_error == :only_unsupported
|
77
|
+
exists ? true : false
|
78
|
+
else
|
79
|
+
!!raise_error
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def request?
|
84
|
+
!!request
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Util
|
3
|
+
# @api private
|
4
|
+
class FieldParams
|
5
|
+
def self.parse(params)
|
6
|
+
return {} if params.nil?
|
7
|
+
|
8
|
+
{}.tap do |parsed|
|
9
|
+
params.each_pair do |key, value|
|
10
|
+
parsed[key.to_sym] = value.split(',').map(&:to_sym)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Util
|
3
|
+
# @api private
|
4
|
+
class Hash
|
5
|
+
# Grab all keys at any level of the hash.
|
6
|
+
#
|
7
|
+
# { foo: { bar: { baz: {} } } }
|
8
|
+
#
|
9
|
+
# Becomes
|
10
|
+
#
|
11
|
+
# [:foo, :bar, :bar]
|
12
|
+
#
|
13
|
+
# @param hash the hash we want to process
|
14
|
+
# @param [Array<Symbol, String>] collection the memoized collection of keys
|
15
|
+
# @return [Array<Symbol, String>] the keys
|
16
|
+
# @api private
|
17
|
+
def self.keys(hash, collection = [])
|
18
|
+
hash.each_pair do |key, value|
|
19
|
+
collection << key
|
20
|
+
keys(value, collection)
|
21
|
+
end
|
22
|
+
|
23
|
+
collection
|
24
|
+
end
|
25
|
+
|
26
|
+
# Like ActiveSupport's #deep_merge
|
27
|
+
# @return [Hash] the merged hash
|
28
|
+
# @api private
|
29
|
+
def self.deep_merge!(hash, other)
|
30
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
31
|
+
hash.merge!(other, &merger)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Like ActiveSupport's #deep_dup
|
35
|
+
# @api private
|
36
|
+
def self.deep_dup(hash)
|
37
|
+
if hash.respond_to?(:deep_dup)
|
38
|
+
hash.deep_dup
|
39
|
+
else
|
40
|
+
{}.tap do |duped|
|
41
|
+
hash.each_pair do |key, value|
|
42
|
+
value = deep_dup(value) if value.is_a?(Hash)
|
43
|
+
value = value.dup if value && value.respond_to?(:dup) && ![Symbol, Fixnum].include?(value.class)
|
44
|
+
duped[key] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Util
|
3
|
+
class Hooks
|
4
|
+
def self.record
|
5
|
+
self.hooks = []
|
6
|
+
begin
|
7
|
+
yield.tap { run }
|
8
|
+
ensure
|
9
|
+
self.hooks = []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self._hooks
|
14
|
+
Thread.current[:_compliable_hooks] ||= []
|
15
|
+
end
|
16
|
+
private_class_method :_hooks
|
17
|
+
|
18
|
+
def self.hooks=(val)
|
19
|
+
Thread.current[:_compliable_hooks] = val
|
20
|
+
end
|
21
|
+
|
22
|
+
# Because hooks will be added from the outer edges of
|
23
|
+
# the graph, working inwards
|
24
|
+
def self.add(prc)
|
25
|
+
_hooks.unshift(prc)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.run
|
29
|
+
_hooks.each { |h| h.call }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Util
|
3
|
+
# Utility class for dealing with Include Directives
|
4
|
+
class IncludeParams
|
5
|
+
class << self
|
6
|
+
# Let's say the user requested these sideloads:
|
7
|
+
#
|
8
|
+
# GET /posts?include=comments.author
|
9
|
+
#
|
10
|
+
# But our resource had this code:
|
11
|
+
#
|
12
|
+
# sideload_whitelist({ index: [:comments] })
|
13
|
+
#
|
14
|
+
# We should drop the 'author' sideload from the request.
|
15
|
+
#
|
16
|
+
# Hashes become 'include directive hashes' within the library. ie
|
17
|
+
#
|
18
|
+
# [:tags, { comments: :author }]
|
19
|
+
#
|
20
|
+
# Becomes
|
21
|
+
#
|
22
|
+
# { tags: {}, comments: { author: {} } }
|
23
|
+
#
|
24
|
+
# @param [Hash] requested_includes the nested hash the user requested
|
25
|
+
# @param [Hash] allowed_includes the nested hash configured via DSL
|
26
|
+
# @return [Hash] the scrubbed hash
|
27
|
+
def scrub(requested_includes, allowed_includes)
|
28
|
+
{}.tap do |valid|
|
29
|
+
requested_includes.each_pair do |key, sub_hash|
|
30
|
+
if allowed_includes[key]
|
31
|
+
valid[key] = scrub(sub_hash, allowed_includes[key])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# Save the given Resource#model, and all of its nested relationships.
|
2
|
+
# @api private
|
3
|
+
class Graphiti::Util::Persistence
|
4
|
+
# @param [Resource] resource the resource instance
|
5
|
+
# @param [Hash] meta see (Deserializer#meta)
|
6
|
+
# @param [Hash] attributes see (Deserializer#attributes)
|
7
|
+
# @param [Hash] relationships see (Deserializer#relationships)
|
8
|
+
def initialize(resource, meta, attributes, relationships, caller_model)
|
9
|
+
@resource = resource
|
10
|
+
@meta = meta
|
11
|
+
@attributes = attributes
|
12
|
+
@relationships = relationships
|
13
|
+
@caller_model = caller_model
|
14
|
+
|
15
|
+
@attributes.each_pair do |key, value|
|
16
|
+
@attributes[key] = @resource.typecast(key, value, :writable)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Perform the actual save logic.
|
21
|
+
#
|
22
|
+
# belongs_to must be processed before/separately from has_many -
|
23
|
+
# we need to know the primary key value of the parent before
|
24
|
+
# persisting the child.
|
25
|
+
#
|
26
|
+
# Flow:
|
27
|
+
# * process parents
|
28
|
+
# * update attributes to reflect parent primary keys
|
29
|
+
# * persist current object
|
30
|
+
# * associate temp id with current object
|
31
|
+
# * associate parent objects with current object
|
32
|
+
# * process children
|
33
|
+
# * associate children
|
34
|
+
# * record hooks for later playback
|
35
|
+
# * run post-process sideload hooks
|
36
|
+
# * return current object
|
37
|
+
#
|
38
|
+
# @return a model instance
|
39
|
+
def run
|
40
|
+
parents = process_belongs_to(@relationships)
|
41
|
+
update_foreign_key_for_parents(parents)
|
42
|
+
|
43
|
+
persisted = persist_object(@meta[:method], @attributes)
|
44
|
+
persisted.instance_variable_set(:@__serializer_klass, @resource.serializer)
|
45
|
+
assign_temp_id(persisted, @meta[:temp_id])
|
46
|
+
|
47
|
+
associate_parents(persisted, parents)
|
48
|
+
|
49
|
+
children = process_has_many(@relationships, persisted) do |x|
|
50
|
+
update_foreign_key(persisted, x[:attributes], x)
|
51
|
+
end
|
52
|
+
|
53
|
+
associate_children(persisted, children) unless @meta[:method] == :destroy
|
54
|
+
|
55
|
+
post_process(persisted, parents)
|
56
|
+
post_process(persisted, children)
|
57
|
+
before_commit = -> { @resource.before_commit(persisted, @meta[:method]) }
|
58
|
+
add_hook(before_commit)
|
59
|
+
persisted
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def add_hook(prc)
|
65
|
+
::Graphiti::Util::Hooks.add(prc)
|
66
|
+
end
|
67
|
+
|
68
|
+
# The child's attributes should be modified to nil-out the
|
69
|
+
# foreign_key when the parent is being destroyed or disassociated
|
70
|
+
#
|
71
|
+
# This is not the case for HABTM, whose "foreign key" is a join table
|
72
|
+
def update_foreign_key(parent_object, attrs, x)
|
73
|
+
return if x[:sideload].type == :many_to_many
|
74
|
+
|
75
|
+
if [:destroy, :disassociate].include?(x[:meta][:method])
|
76
|
+
attrs[x[:foreign_key]] = nil
|
77
|
+
update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
|
78
|
+
else
|
79
|
+
attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
|
80
|
+
update_foreign_type(attrs, x) if x[:is_polymorphic]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_foreign_type(attrs, x, null: false)
|
85
|
+
grouping_field = x[:sideload].parent.grouper.field_name
|
86
|
+
attrs[grouping_field] = null ? nil : x[:sideload].group_name
|
87
|
+
end
|
88
|
+
|
89
|
+
def update_foreign_key_for_parents(parents)
|
90
|
+
parents.each do |x|
|
91
|
+
update_foreign_key(x[:object], @attributes, x)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def associate_parents(object, parents)
|
96
|
+
# No need to associate to destroyed objects
|
97
|
+
parents = parents.select { |x| x[:meta][:method] != :destroy }
|
98
|
+
|
99
|
+
parents.each do |x|
|
100
|
+
if x[:object] && object
|
101
|
+
if x[:meta][:method] == :disassociate
|
102
|
+
if x[:sideload].type == :belongs_to
|
103
|
+
x[:sideload].disassociate(object, x[:object])
|
104
|
+
else
|
105
|
+
x[:sideload].disassociate(x[:object], object)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
if x[:sideload].type == :belongs_to
|
109
|
+
x[:sideload].associate(object, x[:object])
|
110
|
+
else
|
111
|
+
if [:has_many, :many_to_many].include?(x[:sideload].type)
|
112
|
+
x[:sideload].associate_all(object, Array(x[:object]))
|
113
|
+
else
|
114
|
+
x[:sideload].associate(x[:object], object)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def associate_children(object, children)
|
123
|
+
children.each do |x|
|
124
|
+
if x[:object] && object
|
125
|
+
if x[:meta][:method] == :disassociate
|
126
|
+
x[:sideload].disassociate(object, x[:object])
|
127
|
+
elsif x[:meta][:method] == :destroy
|
128
|
+
if x[:sideload].type == :many_to_many
|
129
|
+
x[:sideload].disassociate(object, x[:object])
|
130
|
+
end # otherwise, no need to disassociate destroyed objects
|
131
|
+
else
|
132
|
+
if [:has_many, :many_to_many].include?(x[:sideload].type)
|
133
|
+
x[:sideload].associate_all(object, Array(x[:object]))
|
134
|
+
else
|
135
|
+
x[:sideload].associate(object, x[:object])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def persist_object(method, attributes)
|
143
|
+
case method
|
144
|
+
when :destroy
|
145
|
+
call_resource_method(:destroy, attributes[:id], @caller_model)
|
146
|
+
when :update, nil, :disassociate
|
147
|
+
call_resource_method(:update, attributes, @caller_model)
|
148
|
+
else
|
149
|
+
call_resource_method(:create, attributes, @caller_model)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def process_has_many(relationships, caller_model)
|
154
|
+
[].tap do |processed|
|
155
|
+
iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
|
156
|
+
yield x
|
157
|
+
|
158
|
+
x[:object] = x[:sideload].resource
|
159
|
+
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model)
|
160
|
+
processed << x
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def process_belongs_to(relationships)
|
166
|
+
[].tap do |processed|
|
167
|
+
iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
|
168
|
+
x[:object] = x[:sideload].resource
|
169
|
+
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
|
170
|
+
processed << x
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def post_process(caller_model, processed)
|
176
|
+
groups = processed.group_by { |x| x[:meta][:method] }
|
177
|
+
groups.each_pair do |method, group|
|
178
|
+
group.group_by { |g| g[:sideload] }.each_pair do |sideload, members|
|
179
|
+
objects = members.map { |x| x[:object] }
|
180
|
+
hook = -> { sideload.fire_hooks!(caller_model, objects, method) }
|
181
|
+
add_hook(hook)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def assign_temp_id(object, temp_id)
|
187
|
+
object.instance_variable_set(:@_jsonapi_temp_id, temp_id)
|
188
|
+
end
|
189
|
+
|
190
|
+
def iterate(only: [], except: [])
|
191
|
+
opts = {
|
192
|
+
resource: @resource,
|
193
|
+
relationships: @relationships,
|
194
|
+
}.merge(only: only, except: except)
|
195
|
+
|
196
|
+
Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
|
197
|
+
yield x
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# In the Resource, we want to allow:
|
202
|
+
#
|
203
|
+
# def create(attrs)
|
204
|
+
#
|
205
|
+
# and
|
206
|
+
#
|
207
|
+
# def create(attrs, parent = nil)
|
208
|
+
#
|
209
|
+
# 'parent' is an optional parameter that should not be part of the
|
210
|
+
# method signature in most use cases.
|
211
|
+
def call_resource_method(method_name, attributes, caller_model)
|
212
|
+
method = @resource.method(method_name)
|
213
|
+
if [2,-2].include?(method.arity)
|
214
|
+
method.call(attributes, caller_model)
|
215
|
+
else
|
216
|
+
method.call(attributes)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|