datamappify 0.20.1 → 0.30.0
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/.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
|
+
|