datamappify 0.20.1 → 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +10 -4
- data/README.md +28 -33
- data/datamappify.gemspec +3 -1
- data/lib/datamappify/data/criteria/active_record/count.rb +12 -0
- data/lib/datamappify/data/criteria/active_record/destroy.rb +17 -0
- data/lib/datamappify/data/criteria/active_record/exists.rb +13 -0
- data/lib/datamappify/data/criteria/active_record/find.rb +12 -0
- data/lib/datamappify/data/criteria/active_record/find_by_key.rb +12 -0
- data/lib/datamappify/data/criteria/active_record/save.rb +23 -0
- data/lib/datamappify/data/criteria/active_record/save_by_key.rb +14 -0
- data/lib/datamappify/data/criteria/active_record/transaction.rb +13 -0
- data/lib/datamappify/data/criteria/common.rb +96 -0
- data/lib/datamappify/data/criteria/relational/count.rb +13 -0
- data/lib/datamappify/data/criteria/relational/find.rb +23 -0
- data/lib/datamappify/data/criteria/relational/find_by_key.rb +17 -0
- data/lib/datamappify/data/criteria/relational/save.rb +26 -0
- data/lib/datamappify/data/criteria/relational/save_by_key.rb +15 -0
- data/lib/datamappify/data/criteria/sequel/count.rb +12 -0
- data/lib/datamappify/data/criteria/sequel/destroy.rb +17 -0
- data/lib/datamappify/data/criteria/sequel/exists.rb +13 -0
- data/lib/datamappify/data/criteria/sequel/find.rb +12 -0
- data/lib/datamappify/data/criteria/sequel/find_by_key.rb +12 -0
- data/lib/datamappify/data/criteria/sequel/save.rb +23 -0
- data/lib/datamappify/data/criteria/sequel/save_by_key.rb +14 -0
- data/lib/datamappify/data/criteria/sequel/transaction.rb +13 -0
- data/lib/datamappify/data/criteria.rb +8 -0
- data/lib/datamappify/data/errors.rb +1 -0
- data/lib/datamappify/data/mapper/attribute.rb +52 -0
- data/lib/datamappify/data/mapper.rb +95 -0
- data/lib/datamappify/data/provider/active_record.rb +7 -7
- data/lib/datamappify/data/provider/common_provider.rb +67 -0
- data/lib/datamappify/data/provider/sequel.rb +9 -9
- data/lib/datamappify/data/provider.rb +12 -0
- data/lib/datamappify/data/record.rb +13 -0
- data/lib/datamappify/data.rb +4 -0
- data/lib/datamappify/repository/mapping_dsl.rb +28 -0
- data/lib/datamappify/repository/query_method/count.rb +12 -0
- data/lib/datamappify/repository/query_method/destroy.rb +25 -0
- data/lib/datamappify/repository/query_method/find.rb +41 -0
- data/lib/datamappify/repository/query_method/method.rb +73 -0
- data/lib/datamappify/repository/query_method/save.rb +42 -0
- data/lib/datamappify/repository/query_method/transaction.rb +18 -0
- data/lib/datamappify/repository/query_method.rb +3 -0
- data/lib/datamappify/repository.rb +58 -92
- data/lib/datamappify/version.rb +1 -1
- data/lib/datamappify.rb +10 -5
- data/spec/repository/persistence_spec.rb +15 -23
- data/spec/repository_spec.rb +9 -5
- metadata +70 -15
- data/lib/datamappify/data/provider/active_record/persistence.rb +0 -31
- data/lib/datamappify/data/provider/common/persistence.rb +0 -57
- data/lib/datamappify/data/provider/common/relational/persistence.rb +0 -85
- data/lib/datamappify/data/provider/common/relational/record/mapper.rb +0 -24
- data/lib/datamappify/data/provider/common/relational/record/writer.rb +0 -67
- data/lib/datamappify/data/provider/sequel/persistence.rb +0 -31
- data/lib/datamappify/repository/attribute_source_data_class_builder.rb +0 -28
- data/lib/datamappify/repository/attributes_mapper.rb +0 -55
- data/lib/datamappify/repository/dsl.rb +0 -22
- data/lib/datamappify/repository/mapping_hash.rb +0 -8
- data/lib/datamappify/util.rb +0 -13
@@ -0,0 +1,52 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Data
|
3
|
+
class Mapper
|
4
|
+
# Represents an entity attribute and its associated data source
|
5
|
+
class Attribute
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :provider_name
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :source_class_name
|
14
|
+
|
15
|
+
# @return [String]
|
16
|
+
attr_reader :source_attribute_name
|
17
|
+
|
18
|
+
# @param name [Symbol]
|
19
|
+
# name of the attribute
|
20
|
+
#
|
21
|
+
# @param source [String]
|
22
|
+
# data provider, class and attribute,
|
23
|
+
# e.g. "ActiveRecord::User#surname"
|
24
|
+
def initialize(name, source)
|
25
|
+
@name = name.to_s
|
26
|
+
|
27
|
+
@provider_name, @source_class_name, @source_attribute_name = parse_source(source)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Class]
|
31
|
+
def source_class
|
32
|
+
@source_class ||= Record.find_or_build(provider_name, source_class_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean]
|
36
|
+
def primary_key?
|
37
|
+
source_attribute_name == 'id'
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# @return [Array<String>]
|
43
|
+
# an array with provider name, source class name and source attribute name
|
44
|
+
def parse_source(source)
|
45
|
+
provider_name, source_class_and_attribute = source.split('::')
|
46
|
+
|
47
|
+
[provider_name, *source_class_and_attribute.split('#')]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'datamappify/data/mapper/attribute'
|
3
|
+
|
4
|
+
module Datamappify
|
5
|
+
module Data
|
6
|
+
class Mapper
|
7
|
+
# @return [Class]
|
8
|
+
attr_accessor :entity_class
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
attr_accessor :default_provider_name
|
12
|
+
|
13
|
+
# @return [Hash]
|
14
|
+
# attribute name to source mapping as specified in {Repository::MappingDSL#map_attribute}
|
15
|
+
attr_accessor :custom_mapping
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@custom_mapping = {}
|
19
|
+
@custom_attribute_names = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Module]
|
23
|
+
def default_provider
|
24
|
+
@default_provider ||= Provider.const_get(default_provider_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Module]
|
28
|
+
def provider(provider_name)
|
29
|
+
Provider.const_get(provider_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Class]
|
33
|
+
def default_source_class
|
34
|
+
@default_source_class ||= default_provider.find_or_build_record_class(entity_class.name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Hash]
|
38
|
+
# attribute sets classified by the names of their data provider
|
39
|
+
def classified_attributes
|
40
|
+
@classified_attributes ||= Set.new(custom_attributes + default_attributes).classify(&:provider_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @return [Array<Symbol>]
|
46
|
+
def all_attribute_names
|
47
|
+
entity_class.attribute_set.entries.collect(&:name)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array<Symbol>]
|
51
|
+
def default_attribute_names
|
52
|
+
all_attribute_names - custom_attribute_names
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Array<Symbol>]
|
56
|
+
def custom_attribute_names
|
57
|
+
# make sure custom attributes are always processed
|
58
|
+
custom_attributes
|
59
|
+
|
60
|
+
@custom_attribute_names
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Array<Attribute>]
|
64
|
+
def default_attributes
|
65
|
+
@default_attributes ||= default_attribute_names.collect do |attribute|
|
66
|
+
Attribute.new(attribute, default_source_for(attribute))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Array<Attribute>]
|
71
|
+
def custom_attributes
|
72
|
+
@custom_attributes ||= custom_mapping.collect do |attribute, source|
|
73
|
+
map_attribute(attribute, source)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param (see Data::Mapper::Attribute#initialize)
|
78
|
+
#
|
79
|
+
# @return [Attribute]
|
80
|
+
def map_attribute(name, source)
|
81
|
+
@custom_attribute_names << name
|
82
|
+
|
83
|
+
Attribute.new(name, source)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param attribute [Symbol]
|
87
|
+
# name of the attribute
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
def default_source_for(attribute)
|
91
|
+
"#{default_provider_name}::#{entity_class.name}##{attribute}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -1,14 +1,14 @@
|
|
1
|
-
require 'datamappify/data/provider/active_record/persistence'
|
2
|
-
|
3
1
|
module Datamappify
|
4
2
|
module Data
|
5
|
-
module ActiveRecord
|
6
|
-
end
|
7
|
-
|
8
3
|
module Provider
|
9
4
|
module ActiveRecord
|
10
|
-
|
11
|
-
|
5
|
+
extend CommonProvider
|
6
|
+
|
7
|
+
# @return [ActiveRecord::Base]
|
8
|
+
def self.build_record_class(source_class_name)
|
9
|
+
Datamappify::Data::Record::ActiveRecord.const_set(
|
10
|
+
source_class_name, Class.new(::ActiveRecord::Base)
|
11
|
+
)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Data
|
3
|
+
module Provider
|
4
|
+
module CommonProvider
|
5
|
+
def self.extended(klass)
|
6
|
+
klass.extend ModuleMethods
|
7
|
+
|
8
|
+
klass.load_criterias
|
9
|
+
end
|
10
|
+
|
11
|
+
module ModuleMethods
|
12
|
+
# Loads all the criteria files from the data provider
|
13
|
+
#
|
14
|
+
# @return [void]
|
15
|
+
def load_criterias
|
16
|
+
Dir[Datamappify.root.join("data/criteria/#{path_name}/*.rb")].each { |file| require file }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Non-namespaced class name
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
def class_name
|
23
|
+
@class_name ||= name.demodulize
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
def path_name
|
28
|
+
@path_name ||= class_name.underscore
|
29
|
+
end
|
30
|
+
|
31
|
+
# Finds or builds a data record class from the data provider
|
32
|
+
#
|
33
|
+
# @return [Class]
|
34
|
+
# the data record class
|
35
|
+
def find_or_build_record_class(source_class_name)
|
36
|
+
if records_namespace.const_defined?(source_class_name, false)
|
37
|
+
records_namespace.const_get(source_class_name)
|
38
|
+
else
|
39
|
+
build_record_class(source_class_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# The namespace for the data records, e.g. +Datamappify::Data::Record::ActiveRecord+
|
46
|
+
#
|
47
|
+
# @return [Module]
|
48
|
+
def records_namespace
|
49
|
+
@records_namespace ||= Data::Record.const_set(class_name, Module.new)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Builds a {Criteria}
|
54
|
+
#
|
55
|
+
# @param name [Symbol]
|
56
|
+
#
|
57
|
+
# @param args [any]
|
58
|
+
#
|
59
|
+
# @yield
|
60
|
+
# an optional block passed to the +Criteria+ {Criteria::Common#initialize initialiser}
|
61
|
+
def build_criteria(name, *args, &block)
|
62
|
+
Data::Criteria.const_get(class_name).const_get(name).new(*args, &block).result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
|
-
require 'datamappify/data/provider/sequel/persistence'
|
2
|
-
|
3
1
|
module Datamappify
|
4
2
|
module Data
|
5
|
-
module Sequel
|
6
|
-
end
|
7
|
-
|
8
3
|
module Provider
|
9
4
|
module Sequel
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
extend CommonProvider
|
6
|
+
|
7
|
+
# @return [Sequel::Model]
|
8
|
+
def self.build_record_class(source_class_name)
|
9
|
+
Record::Sequel.const_set(
|
10
|
+
source_class_name, Class.new(::Sequel::Model(source_class_name.pluralize.underscore.to_sym))
|
11
|
+
).tap do |klass|
|
12
|
+
klass.raise_on_save_failure = true
|
13
|
+
end
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Data
|
3
|
+
# A convenient class for finding or building a data record
|
4
|
+
module Record
|
5
|
+
# @param provider_name [String]
|
6
|
+
#
|
7
|
+
# @param source_class_name [String]
|
8
|
+
def self.find_or_build(provider_name, source_class_name)
|
9
|
+
Provider.const_get(provider_name).find_or_build_record_class(source_class_name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/datamappify/data.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module MappingDSL
|
4
|
+
# @param entity_class [Class]
|
5
|
+
# entity class
|
6
|
+
#
|
7
|
+
# @return [void]
|
8
|
+
def for_entity(entity_class)
|
9
|
+
data_mapper.entity_class = entity_class
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param provider_name [String]
|
13
|
+
# name of data provider
|
14
|
+
#
|
15
|
+
# @return [void]
|
16
|
+
def default_provider(provider_name)
|
17
|
+
data_mapper.default_provider_name = provider_name.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param (see Data::Mapper::Attribute#initialize)
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def map_attribute(name, source)
|
24
|
+
data_mapper.custom_mapping[name] = source
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module QueryMethod
|
4
|
+
class Destroy < Method
|
5
|
+
# @param mapper (see Method#initialize)
|
6
|
+
#
|
7
|
+
# @param id_or_ids_or_entity_or_entities [Entity, Array<Entity>]
|
8
|
+
# an entity or a collection of ids or entities
|
9
|
+
def initialize(mapper, id_or_ids_or_entity_or_entities)
|
10
|
+
super
|
11
|
+
@id_or_ids_or_entity_or_entities = id_or_ids_or_entity_or_entities
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [void, false]
|
15
|
+
def result
|
16
|
+
entities = Array.wrap(@id_or_ids_or_entity_or_entities).map do |id_or_entity|
|
17
|
+
dispatch_criteria_to_default_source(:Destroy, extract_entity_id(id_or_entity))
|
18
|
+
end
|
19
|
+
|
20
|
+
@id_or_ids_or_entity_or_entities.is_a?(Array) ? entities : entities[0]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module QueryMethod
|
4
|
+
class Find < Method
|
5
|
+
# @param mapper (see Method#initialize)
|
6
|
+
#
|
7
|
+
# @param id_or_ids [Integer, Array<Integer>]
|
8
|
+
# an entity id or a collection of entity ids
|
9
|
+
def initialize(mapper, id_or_ids)
|
10
|
+
super
|
11
|
+
@id_or_ids = id_or_ids
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Entity, Array<Entity>, nil]
|
15
|
+
def result
|
16
|
+
entities = Array.wrap(@id_or_ids).map { |id| setup_new_entity(id) }.compact
|
17
|
+
|
18
|
+
@id_or_ids.is_a?(Array) ? entities : entities[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# @param id [Integer]
|
24
|
+
#
|
25
|
+
# @return [Entity, nil]
|
26
|
+
def setup_new_entity(id)
|
27
|
+
entity = @mapper.entity_class.new
|
28
|
+
entity.id = id
|
29
|
+
|
30
|
+
if dispatch_criteria_to_default_source(:Exists, entity)
|
31
|
+
dispatch_criteria_to_providers(:FindByKey, entity)
|
32
|
+
else
|
33
|
+
entity = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
entity
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module QueryMethod
|
4
|
+
# Provides a default set of methods to the varies {QueryMethod} classes
|
5
|
+
class Method
|
6
|
+
# @param mapper [Data::Mapper]
|
7
|
+
#
|
8
|
+
# @param args [any]
|
9
|
+
def initialize(mapper, *args)
|
10
|
+
@mapper = mapper
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Dispatches a {Criteria} according to
|
16
|
+
# the {Data::Mapper data mapper}'s default provider and default source class
|
17
|
+
#
|
18
|
+
# @param criteria_name [Symbol]
|
19
|
+
#
|
20
|
+
# @param args [any]
|
21
|
+
def dispatch_criteria_to_default_source(criteria_name, *args)
|
22
|
+
@mapper.default_provider.build_criteria(criteria_name, @mapper.default_source_class, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Dispatches a {Criteria} via {#attributes_walker}
|
26
|
+
#
|
27
|
+
# @param criteria_name [Symbol]
|
28
|
+
#
|
29
|
+
# @param entity [Entity]
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
def dispatch_criteria_to_providers(criteria_name, entity)
|
33
|
+
attributes_walker do |provider_name, source_class, attributes|
|
34
|
+
@mapper.provider(provider_name).build_criteria(
|
35
|
+
criteria_name, source_class, entity, attributes
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Walks through the attributes and performs actions on them
|
41
|
+
#
|
42
|
+
# @yield [provider_name, source_class, attributes]
|
43
|
+
# action to be performed on the attributes grouped by their source class
|
44
|
+
#
|
45
|
+
# @yieldparam provider_name [String]
|
46
|
+
#
|
47
|
+
# @yieldparam source_class [Class]
|
48
|
+
#
|
49
|
+
# @yieldparam attributes [Set]
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def attributes_walker(&block)
|
53
|
+
Transaction.new(@mapper) do
|
54
|
+
@mapper.classified_attributes.each do |provider_name, attributes|
|
55
|
+
attributes.classify(&:source_class).each do |source_class, attrs|
|
56
|
+
block.call(provider_name, source_class, attrs)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Extract the id out of an entity, unless the argument is already an id
|
63
|
+
#
|
64
|
+
# @param id_or_entity [Entity, Integer]
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
def extract_entity_id(id_or_entity)
|
68
|
+
id_or_entity.is_a?(Integer) ? id_or_entity : id_or_entity.id
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module QueryMethod
|
4
|
+
class Save < Method
|
5
|
+
# @param mapper (see Method#initialize)
|
6
|
+
#
|
7
|
+
# @param entity_or_entities [Entity, Array<Entity>]
|
8
|
+
# an entity or a collection of entities
|
9
|
+
def initialize(mapper, entity_or_entities)
|
10
|
+
super
|
11
|
+
@entity_or_entities = entity_or_entities
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Entity, Array<Entity>, false]
|
15
|
+
def result
|
16
|
+
Array.wrap(@entity_or_entities).each do |entity|
|
17
|
+
create_or_update(entity)
|
18
|
+
end
|
19
|
+
|
20
|
+
@entity_or_entities
|
21
|
+
rescue Data::EntityInvalid
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param entity [Entity]
|
28
|
+
#
|
29
|
+
# @raise [Data::EntityInvalid]
|
30
|
+
#
|
31
|
+
# @return [Entity]
|
32
|
+
def create_or_update(entity)
|
33
|
+
raise Data::EntityInvalid.new(entity) if entity.invalid?
|
34
|
+
|
35
|
+
dispatch_criteria_to_providers(:SaveByKey, entity)
|
36
|
+
|
37
|
+
entity
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Datamappify
|
2
|
+
module Repository
|
3
|
+
module QueryMethod
|
4
|
+
class Transaction < Method
|
5
|
+
# @param mapper (see Method#initialize)
|
6
|
+
#
|
7
|
+
# @yield
|
8
|
+
# queries to be performed in the transaction
|
9
|
+
#
|
10
|
+
# @return [void]
|
11
|
+
def initialize(mapper, &block)
|
12
|
+
mapper.default_provider.build_criteria(:Transaction, mapper.default_source_class, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|