rom 0.4.2 → 0.5.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/.rubocop.yml +81 -0
- data/.travis.yml +2 -1
- data/CHANGELOG.md +41 -0
- data/Gemfile +12 -8
- data/Guardfile +17 -11
- data/README.md +7 -7
- data/Rakefile +29 -0
- data/lib/rom.rb +9 -66
- data/lib/rom/adapter.rb +45 -12
- data/lib/rom/adapter/memory.rb +0 -4
- data/lib/rom/adapter/memory/commands.rb +0 -10
- data/lib/rom/adapter/memory/dataset.rb +18 -6
- data/lib/rom/adapter/memory/storage.rb +0 -3
- data/lib/rom/command_registry.rb +24 -43
- data/lib/rom/commands.rb +5 -6
- data/lib/rom/commands/create.rb +5 -5
- data/lib/rom/commands/delete.rb +8 -6
- data/lib/rom/commands/result.rb +82 -0
- data/lib/rom/commands/update.rb +5 -4
- data/lib/rom/commands/with_options.rb +1 -4
- data/lib/rom/config.rb +70 -0
- data/lib/rom/env.rb +11 -3
- data/lib/rom/global.rb +107 -0
- data/lib/rom/header.rb +122 -89
- data/lib/rom/header/attribute.rb +148 -0
- data/lib/rom/mapper.rb +46 -67
- data/lib/rom/mapper_builder.rb +20 -73
- data/lib/rom/mapper_builder/mapper_dsl.rb +114 -0
- data/lib/rom/mapper_builder/model_dsl.rb +29 -0
- data/lib/rom/mapper_registry.rb +21 -0
- data/lib/rom/model_builder.rb +11 -17
- data/lib/rom/processor.rb +28 -0
- data/lib/rom/processor/transproc.rb +105 -0
- data/lib/rom/reader.rb +81 -21
- data/lib/rom/reader_builder.rb +14 -4
- data/lib/rom/relation.rb +19 -5
- data/lib/rom/relation_builder.rb +20 -6
- data/lib/rom/repository.rb +0 -2
- data/lib/rom/setup.rb +156 -0
- data/lib/rom/{boot → setup}/base_relation_dsl.rb +4 -8
- data/lib/rom/setup/command_dsl.rb +46 -0
- data/lib/rom/setup/finalize.rb +125 -0
- data/lib/rom/setup/mapper_dsl.rb +19 -0
- data/lib/rom/{boot → setup}/relation_dsl.rb +1 -4
- data/lib/rom/setup/schema_dsl.rb +33 -0
- data/lib/rom/support/registry.rb +10 -6
- data/lib/rom/version.rb +1 -1
- data/rom.gemspec +3 -1
- data/spec/integration/adapters/extending_relations_spec.rb +0 -2
- data/spec/integration/commands/create_spec.rb +2 -9
- data/spec/integration/commands/delete_spec.rb +4 -5
- data/spec/integration/commands/error_handling_spec.rb +4 -3
- data/spec/integration/commands/update_spec.rb +3 -8
- data/spec/integration/mappers/deep_embedded_spec.rb +52 -0
- data/spec/integration/mappers/definition_dsl_spec.rb +0 -118
- data/spec/integration/mappers/embedded_spec.rb +82 -0
- data/spec/integration/mappers/group_spec.rb +170 -0
- data/spec/integration/mappers/prefixing_attributes_spec.rb +2 -2
- data/spec/integration/mappers/renaming_attributes_spec.rb +8 -6
- data/spec/integration/mappers/symbolizing_attributes_spec.rb +80 -0
- data/spec/integration/mappers/wrap_spec.rb +162 -0
- data/spec/integration/multi_repo_spec.rb +64 -0
- data/spec/integration/relations/reading_spec.rb +12 -8
- data/spec/integration/relations/registry_dsl_spec.rb +1 -3
- data/spec/integration/schema_spec.rb +10 -0
- data/spec/integration/setup_spec.rb +57 -6
- data/spec/spec_helper.rb +2 -1
- data/spec/unit/config_spec.rb +60 -0
- data/spec/unit/rom/adapter/memory/dataset_spec.rb +52 -0
- data/spec/unit/rom/adapter_spec.rb +31 -11
- data/spec/unit/rom/header_spec.rb +60 -16
- data/spec/unit/rom/mapper_builder_spec.rb +311 -0
- data/spec/unit/rom/mapper_registry_spec.rb +25 -0
- data/spec/unit/rom/mapper_spec.rb +4 -5
- data/spec/unit/rom/model_builder_spec.rb +15 -13
- data/spec/unit/rom/processor/transproc_spec.rb +331 -0
- data/spec/unit/rom/reader_spec.rb +73 -0
- data/spec/unit/rom/registry_spec.rb +38 -0
- data/spec/unit/rom/relation_spec.rb +0 -1
- data/spec/unit/rom/setup_spec.rb +55 -0
- data/spec/unit/rom_spec.rb +14 -0
- metadata +62 -22
- data/Gemfile.devtools +0 -71
- data/lib/rom/boot.rb +0 -197
- data/lib/rom/boot/command_dsl.rb +0 -48
- data/lib/rom/boot/dsl.rb +0 -37
- data/lib/rom/boot/mapper_dsl.rb +0 -23
- data/lib/rom/boot/schema_dsl.rb +0 -27
- data/lib/rom/ra.rb +0 -172
- data/lib/rom/ra/operation/group.rb +0 -47
- data/lib/rom/ra/operation/join.rb +0 -39
- data/lib/rom/ra/operation/wrap.rb +0 -45
- data/lib/rom/transformer.rb +0 -77
- data/spec/integration/ra/group_spec.rb +0 -46
- data/spec/integration/ra/join_spec.rb +0 -50
- data/spec/integration/ra/wrap_spec.rb +0 -37
- data/spec/unit/rom/ra/operation/group_spec.rb +0 -55
- data/spec/unit/rom/ra/operation/wrap_spec.rb +0 -29
- data/spec/unit/rom/transformer_spec.rb +0 -41
@@ -0,0 +1,148 @@
|
|
1
|
+
module ROM
|
2
|
+
class Header
|
3
|
+
# An attribute provides information about a specific attribute in a tuple
|
4
|
+
#
|
5
|
+
# This may include information about how an attribute should be renamed,
|
6
|
+
# or how its value should coerced.
|
7
|
+
#
|
8
|
+
# More complex attributes describe how an attribute should be transformed.
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
class Attribute
|
12
|
+
include Equalizer.new(:name, :key, :type)
|
13
|
+
|
14
|
+
# @return [Symbol] name of an attribute
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
# @return [Symbol] key of an attribute that corresponds to tuple attribute
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
attr_reader :key
|
23
|
+
|
24
|
+
# @return [Symbol] type identifier (defaults to :object)
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
attr_reader :type
|
28
|
+
|
29
|
+
# @return [Hash] additional meta information
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
attr_reader :meta
|
33
|
+
|
34
|
+
# Return attribute class for a give meta hash
|
35
|
+
#
|
36
|
+
# @param [Hash] hash with type information and optional transformation info
|
37
|
+
#
|
38
|
+
# @return [Class]
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
def self.[](meta)
|
42
|
+
type = meta[:type]
|
43
|
+
|
44
|
+
if type.equal?(:hash)
|
45
|
+
meta[:wrap] ? Wrap : Hash
|
46
|
+
elsif type.equal?(:array)
|
47
|
+
meta[:group] ? Group : Array
|
48
|
+
else
|
49
|
+
self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Coerce an array with attribute meta-data into an attribute object
|
54
|
+
#
|
55
|
+
# @param [Array<Symbol,Hash>] name/options pair
|
56
|
+
#
|
57
|
+
# @return [Attribute]
|
58
|
+
#
|
59
|
+
# @api private
|
60
|
+
def self.coerce(input)
|
61
|
+
name = input[0]
|
62
|
+
meta = (input[1] || {}).dup
|
63
|
+
|
64
|
+
meta[:type] ||= :object
|
65
|
+
|
66
|
+
if meta.key?(:header)
|
67
|
+
meta[:header] = Header.coerce(meta[:header], meta[:model])
|
68
|
+
end
|
69
|
+
|
70
|
+
self[meta].new(name, meta)
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def initialize(name, meta)
|
75
|
+
@name = name
|
76
|
+
@meta = meta
|
77
|
+
@key = meta.fetch(:from) { name }
|
78
|
+
@type = meta.fetch(:type)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return if an attribute has a specific type identifier
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
def typed?
|
85
|
+
type != :object
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return if an attribute should be aliased
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
def aliased?
|
92
|
+
key != name
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return :key-to-:name mapping hash
|
96
|
+
#
|
97
|
+
# @return [Hash]
|
98
|
+
#
|
99
|
+
# @api private
|
100
|
+
def mapping
|
101
|
+
{ key => name }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Embedded attribute is a special attribute type that has a header
|
106
|
+
#
|
107
|
+
# This is the base of complex attributes like Hash or Group
|
108
|
+
#
|
109
|
+
# @private
|
110
|
+
class Embedded < Attribute
|
111
|
+
include Equalizer.new(:name, :key, :type, :header)
|
112
|
+
|
113
|
+
# return [Header] header of an attribute
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
attr_reader :header
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
def initialize(*)
|
120
|
+
super
|
121
|
+
@header = meta.fetch(:header)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return tuple keys from the header
|
125
|
+
#
|
126
|
+
# @return [Array<Symbol>]
|
127
|
+
#
|
128
|
+
# @api private
|
129
|
+
def tuple_keys
|
130
|
+
header.tuple_keys
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Array is an embedded attribute type
|
135
|
+
Array = Class.new(Embedded)
|
136
|
+
|
137
|
+
# Hash is an embedded attribute type
|
138
|
+
Hash = Class.new(Embedded)
|
139
|
+
|
140
|
+
# Wrap is a special type of Hash attribute that requires wrapping
|
141
|
+
# transformation
|
142
|
+
Wrap = Class.new(Hash)
|
143
|
+
|
144
|
+
# Group is a special type of Array attribute that requires grouping
|
145
|
+
# transformation
|
146
|
+
Group = Class.new(Array)
|
147
|
+
end
|
148
|
+
end
|
data/lib/rom/mapper.rb
CHANGED
@@ -1,83 +1,62 @@
|
|
1
1
|
module ROM
|
2
|
-
|
3
|
-
#
|
2
|
+
# Mapper is a simple object that uses a transformer to load relations
|
3
|
+
#
|
4
|
+
# @private
|
4
5
|
class Mapper
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
tuple.map { |key, value| [header.mapping[key], value] }
|
21
|
-
end
|
6
|
+
# @return [Object] transformer object built by a processor
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
attr_reader :transformer
|
10
|
+
|
11
|
+
# @return [Header] header that was used to build the transformer
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
attr_reader :header
|
15
|
+
|
16
|
+
# @return [Hash] registered processors
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def self.processors
|
20
|
+
@_processors ||= {}
|
22
21
|
end
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
def process(relation)
|
33
|
-
transformer.call(relation.to_a).each { |tuple| yield(load(tuple)) }
|
34
|
-
end
|
35
|
-
|
36
|
-
def call(tuple, header = self.header)
|
37
|
-
mapping = header.mapping
|
38
|
-
|
39
|
-
tuple.map do |key, value|
|
40
|
-
case value
|
41
|
-
when Hash
|
42
|
-
[key, loader[Hash[call(value, header[key])], header[key].model]]
|
43
|
-
when Array
|
44
|
-
[key, value.map { |v| loader[Hash[call(v, header[key])], header[key].model] }]
|
45
|
-
else
|
46
|
-
[mapping[key], value]
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
23
|
+
# Register a processor class
|
24
|
+
#
|
25
|
+
# @return [Hash]
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
def self.register_processor(processor)
|
29
|
+
name = processor.name.split('::').last.downcase.to_sym
|
30
|
+
processors.update(name => processor)
|
50
31
|
end
|
51
32
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
self
|
60
|
-
end
|
61
|
-
|
62
|
-
loader = Proc.new { |tuple, m| m ? m.new(tuple) : tuple }
|
63
|
-
|
64
|
-
klass.new(header, model, loader)
|
33
|
+
# Build a mapper using provided processor type
|
34
|
+
#
|
35
|
+
# @return [Mapper]
|
36
|
+
#
|
37
|
+
# @api private
|
38
|
+
def self.build(header, processor = :transproc)
|
39
|
+
new(processors.fetch(processor).build(header), header)
|
65
40
|
end
|
66
41
|
|
67
|
-
|
42
|
+
# @api private
|
43
|
+
def initialize(transformer, header)
|
44
|
+
@transformer = transformer
|
68
45
|
@header = header
|
69
|
-
@model = model
|
70
|
-
@loader = loader
|
71
46
|
end
|
72
47
|
|
73
|
-
|
74
|
-
|
48
|
+
# @return [Class] optional model that is instantiated by a mapper
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
def model
|
52
|
+
header.model
|
75
53
|
end
|
76
54
|
|
77
|
-
|
78
|
-
|
55
|
+
# Process a relation using the transfomer
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
def process(relation, &block)
|
59
|
+
transformer[relation.to_a].each(&block)
|
79
60
|
end
|
80
|
-
|
81
61
|
end
|
82
|
-
|
83
62
|
end
|
data/lib/rom/mapper_builder.rb
CHANGED
@@ -1,105 +1,52 @@
|
|
1
|
-
require 'rom/
|
1
|
+
require 'rom/mapper_builder/model_dsl'
|
2
|
+
require 'rom/mapper_builder/mapper_dsl'
|
2
3
|
|
3
4
|
module ROM
|
4
|
-
|
5
5
|
# @api private
|
6
6
|
class MapperBuilder
|
7
|
+
attr_reader :name, :root, :options, :prefix, :symbolize_keys, :dsl
|
7
8
|
|
8
|
-
|
9
|
-
attr_reader :attributes, :model_class, :model_builder
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@attributes = []
|
13
|
-
end
|
14
|
-
|
15
|
-
def header
|
16
|
-
Header.coerce(attributes)
|
17
|
-
end
|
18
|
-
|
19
|
-
def model(options = nil)
|
20
|
-
if options.is_a?(Class)
|
21
|
-
@model_class = options
|
22
|
-
elsif options
|
23
|
-
@model_builder = ModelBuilder[options.fetch(:type) { :poro }].new(options)
|
24
|
-
end
|
25
|
-
|
26
|
-
if options
|
27
|
-
self
|
28
|
-
else
|
29
|
-
model_class || (model_builder && model_builder.call(header))
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def attribute(name, options = {})
|
34
|
-
attributes << [name, options]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
attr_reader :name, :root, :prefix,
|
39
|
-
:model_builder, :model_class, :attributes
|
9
|
+
DEFAULT_PROCESSOR = :transproc
|
40
10
|
|
41
11
|
def initialize(name, root, options = {})
|
42
12
|
@name = name
|
13
|
+
@options = options
|
43
14
|
@root = root
|
44
15
|
@prefix = options[:prefix]
|
16
|
+
@symbolize_keys = options[:symbolize_keys]
|
45
17
|
|
46
|
-
|
18
|
+
attributes =
|
47
19
|
if options[:inherit_header]
|
48
20
|
root.header.map { |attr| [prefix ? :"#{prefix}_#{attr}" : attr] }
|
49
21
|
else
|
50
22
|
[]
|
51
23
|
end
|
52
|
-
end
|
53
24
|
|
54
|
-
|
55
|
-
if options.is_a?(Class)
|
56
|
-
@model_class = options
|
57
|
-
else
|
58
|
-
@model_builder = ModelBuilder[options.fetch(:type) { :poro }].new(options)
|
59
|
-
end
|
60
|
-
|
61
|
-
self
|
62
|
-
end
|
63
|
-
|
64
|
-
def attribute(name, options = {})
|
65
|
-
options[:from] = :"#{prefix}_#{name}" if prefix
|
66
|
-
attributes << [name, options]
|
67
|
-
end
|
68
|
-
|
69
|
-
def exclude(name)
|
70
|
-
attributes.delete([name])
|
71
|
-
end
|
25
|
+
@dsl = MapperDSL.new(attributes, options)
|
72
26
|
|
73
|
-
|
74
|
-
attribute_dsl(options, Array, &block)
|
27
|
+
@processor = DEFAULT_PROCESSOR
|
75
28
|
end
|
76
29
|
|
77
|
-
def
|
78
|
-
|
30
|
+
def processor(identifier = nil)
|
31
|
+
if identifier
|
32
|
+
@processor = identifier
|
33
|
+
else
|
34
|
+
@processor
|
35
|
+
end
|
79
36
|
end
|
80
37
|
|
81
38
|
def call
|
82
|
-
header
|
83
|
-
|
84
|
-
@model_class = model_builder.call(header) if model_builder
|
85
|
-
|
86
|
-
Mapper.build(header, model_class)
|
39
|
+
Mapper.build(dsl.header, processor)
|
87
40
|
end
|
88
41
|
|
89
42
|
private
|
90
43
|
|
91
|
-
def
|
92
|
-
if
|
93
|
-
dsl
|
94
|
-
dsl.instance_exec(&block)
|
95
|
-
attributes << [options, header: dsl.header, type: type, model: dsl.model]
|
44
|
+
def method_missing(name, *args, &block)
|
45
|
+
if dsl.respond_to?(name)
|
46
|
+
dsl.public_send(name, *args, &block)
|
96
47
|
else
|
97
|
-
|
98
|
-
attributes << [name, header: header.zip, type: type]
|
99
|
-
end
|
48
|
+
super
|
100
49
|
end
|
101
50
|
end
|
102
|
-
|
103
51
|
end
|
104
|
-
|
105
52
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rom/mapper_builder/model_dsl'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class MapperBuilder
|
5
|
+
# @api private
|
6
|
+
class MapperDSL
|
7
|
+
include ModelDSL
|
8
|
+
|
9
|
+
attr_reader :attributes, :options, :symbolize_keys, :prefix
|
10
|
+
|
11
|
+
def initialize(attributes, options)
|
12
|
+
@attributes = attributes
|
13
|
+
@options = options
|
14
|
+
@symbolize_keys = options[:symbolize_keys]
|
15
|
+
@prefix = options[:prefix]
|
16
|
+
end
|
17
|
+
|
18
|
+
def attribute(name, options = EMPTY_HASH)
|
19
|
+
with_attr_options(name, options) do |attr_options|
|
20
|
+
add_attribute(name, attr_options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def exclude(*names)
|
25
|
+
names.each { |name| attributes.delete([name]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def embedded(name, options, &block)
|
29
|
+
with_attr_options(name) do |attr_options|
|
30
|
+
dsl = new(options, &block)
|
31
|
+
|
32
|
+
attr_options.update(options)
|
33
|
+
|
34
|
+
add_attribute(
|
35
|
+
name, { header: dsl.header, type: :array }.update(attr_options)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def wrap(*args, &block)
|
41
|
+
with_name_or_options(*args) do |name, options|
|
42
|
+
dsl(name, { type: :hash, wrap: true }.update(options), &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def group(*args, &block)
|
47
|
+
with_name_or_options(*args) do |name, options|
|
48
|
+
dsl(name, { type: :array, group: true }.update(options), &block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def header
|
53
|
+
Header.coerce(attributes, model)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def with_attr_options(name, options = EMPTY_HASH)
|
59
|
+
attr_options = options.dup
|
60
|
+
|
61
|
+
attr_options[:from] ||= :"#{prefix}_#{name}" if prefix
|
62
|
+
|
63
|
+
if symbolize_keys
|
64
|
+
attr_options.update(from: attr_options.fetch(:from) { name }.to_s)
|
65
|
+
end
|
66
|
+
|
67
|
+
yield(attr_options)
|
68
|
+
end
|
69
|
+
|
70
|
+
def with_name_or_options(*args)
|
71
|
+
name, options =
|
72
|
+
if args.size > 1
|
73
|
+
args
|
74
|
+
else
|
75
|
+
[args.first, {}]
|
76
|
+
end
|
77
|
+
|
78
|
+
yield(name, options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def dsl(name_or_attrs, options, &block)
|
82
|
+
if block
|
83
|
+
attributes_from_block(name_or_attrs, options, &block)
|
84
|
+
else
|
85
|
+
attributes_from_hash(name_or_attrs, options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def attributes_from_block(name, options, &block)
|
90
|
+
dsl = new(options, &block)
|
91
|
+
add_attribute(name, options.update(header: dsl.header))
|
92
|
+
end
|
93
|
+
|
94
|
+
def attributes_from_hash(hash, options)
|
95
|
+
hash.each do |name, header|
|
96
|
+
with_attr_options(name, options) do |attr_options|
|
97
|
+
add_attribute(name, attr_options.update(header: header.zip))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_attribute(name, options)
|
103
|
+
exclude(name, name.to_s)
|
104
|
+
attributes << [name, options]
|
105
|
+
end
|
106
|
+
|
107
|
+
def new(options, &block)
|
108
|
+
dsl = self.class.new([], @options.merge(options))
|
109
|
+
dsl.instance_exec(&block)
|
110
|
+
dsl
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|