abstract_mapper 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +1 -3
- data/README.md +29 -13
- data/Rakefile +4 -7
- data/abstract_mapper.gemspec +3 -2
- data/config/metrics/STYLEGUIDE +14 -15
- data/lib/abstract_mapper.rb +4 -1
- data/lib/abstract_mapper/attributes.rb +65 -0
- data/lib/abstract_mapper/branch.rb +17 -7
- data/lib/abstract_mapper/builder.rb +7 -3
- data/lib/abstract_mapper/command.rb +68 -0
- data/lib/abstract_mapper/commands.rb +11 -31
- data/lib/abstract_mapper/errors/unknown_command.rb +1 -1
- data/lib/abstract_mapper/errors/wrong_node.rb +1 -1
- data/lib/abstract_mapper/errors/wrong_rule.rb +2 -2
- data/lib/abstract_mapper/functions.rb +32 -5
- data/lib/abstract_mapper/node.rb +21 -10
- data/lib/abstract_mapper/optimizer.rb +3 -3
- data/lib/abstract_mapper/pair_rule.rb +2 -4
- data/lib/abstract_mapper/rspec.rb +1 -2
- data/lib/abstract_mapper/rule.rb +23 -2
- data/lib/abstract_mapper/rules.rb +3 -10
- data/lib/abstract_mapper/settings.rb +3 -3
- data/lib/abstract_mapper/sole_rule.rb +2 -4
- data/lib/abstract_mapper/version.rb +1 -1
- data/lib/rspec/doubles.rb +16 -0
- data/lib/rspec/nodes.rb +29 -16
- data/lib/rspec/rules.rb +3 -3
- data/spec/integration/faceter.rb +16 -7
- data/spec/integration/mapper_definition_spec.rb +1 -1
- data/spec/integration/rspec_examples_spec.rb +4 -30
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/abstract_mapper/branch_spec.rb +91 -14
- data/spec/unit/abstract_mapper/builder_spec.rb +66 -48
- data/spec/unit/abstract_mapper/command_spec.rb +92 -0
- data/spec/unit/abstract_mapper/commands_spec.rb +38 -79
- data/spec/unit/abstract_mapper/dsl_spec.rb +60 -55
- data/spec/unit/abstract_mapper/errors/wrong_rule_spec.rb +1 -1
- data/spec/unit/abstract_mapper/functions/compact_spec.rb +3 -3
- data/spec/unit/abstract_mapper/functions/filter_spec.rb +1 -1
- data/spec/unit/abstract_mapper/functions/identity_spec.rb +21 -0
- data/spec/unit/abstract_mapper/functions/restrict_spec.rb +16 -0
- data/spec/unit/abstract_mapper/functions/subclass_spec.rb +1 -1
- data/spec/unit/abstract_mapper/node_spec.rb +119 -48
- data/spec/unit/abstract_mapper/optimizer_spec.rb +38 -32
- data/spec/unit/abstract_mapper/pair_rule_spec.rb +4 -11
- data/spec/unit/abstract_mapper/rule_spec.rb +31 -15
- data/spec/unit/abstract_mapper/rules_spec.rb +2 -2
- data/spec/unit/abstract_mapper/settings_spec.rb +80 -73
- data/spec/unit/abstract_mapper/sole_rule_spec.rb +3 -10
- data/spec/unit/abstract_mapper_spec.rb +13 -5
- metadata +33 -12
- data/lib/rspec/functions.rb +0 -25
- data/lib/rspec/mapper.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0547716c6b580abf30fd1b16d2e42cb2656971da
|
4
|
+
data.tar.gz: 7c793b44bebc3157b32cb754e76fd8e7c550a242
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89772b4b3618585644e6224b93abd13276d1ed057be038ce38c677b752aefacc424fa3d05076810555afe976a45d4e059c82eb96cb7acc2b9f29b96fa66c7f15
|
7
|
+
data.tar.gz: 688f0f055d98a12c7558948b5191c9f1e7fe0d43ac740929339fb3a1e7a700252273a535430c21d3676ecf229ba61257661890711a343f333c66bc9fdce2da5a
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## v0.0.2 2015-08-06
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Coersion of command arguments into the the node's attributes (nepalez)
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
|
9
|
+
* Removed force ordering of rules (nepalez)
|
10
|
+
* Nodes takes hash of attributes instead of array (nepalez)
|
11
|
+
|
12
|
+
### Deleted
|
13
|
+
|
14
|
+
* Removed shared examples for mapper. The mapper should be specified by integration tests (nepalez)
|
15
|
+
|
16
|
+
### Bugs fixed
|
17
|
+
|
18
|
+
* Fixed the `:transforming_immutable_data` shared examples so that it can deal with singleton inputs (nepalez)
|
19
|
+
* Fixed the `:mapping_immutable_input` shared examples so that it can deal with singleton inputs (nepalez)
|
20
|
+
* Fixed typo in `WrongRule` exception message (nepalez)
|
21
|
+
|
22
|
+
### Internal
|
23
|
+
|
24
|
+
* Switched to `transproc` gem v0.3.0 (nepalez)
|
25
|
+
* Added the `Functions#identity` pure function (nepalez)
|
26
|
+
* Made the `Rule` to fully implement the interface, including method `.transproc`, `#optimize?`, `#optimize` that by default change nothing (nepalez)
|
27
|
+
* Made the `.composer` setting private for all rules (`Rule`, `SoleRule`, `PairRule`) (nepalez)
|
28
|
+
* Switched to 'ice_nine' gem for freezing instances deeply (nepalez)
|
29
|
+
* Added `ice_double` feature to specs (nepalez)
|
30
|
+
* Added `AbstractMapper::Command` that can convert arguments of the command to the node's (nepalez)
|
31
|
+
* Switched to `virtus` attributes (nepalez)
|
32
|
+
|
33
|
+
[Compare v0.0.1...v0.0.2](https://github.com/nepalez/abstract_mapper/compare/v0.0.1...v0.0.2)
|
34
|
+
|
1
35
|
## v0.0.1 2015-07-04
|
2
36
|
|
3
37
|
This is the first published version of the gem.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -62,7 +62,7 @@ The following example represents an oversimplified version of the [faceter] gem.
|
|
62
62
|
|
63
63
|
Every node should implement the `#transproc` method that transforms some input data to the output.
|
64
64
|
|
65
|
-
When you need attributes, assign them
|
65
|
+
When you need attributes, assign them using [virtus] method `attribute`:
|
66
66
|
|
67
67
|
```ruby
|
68
68
|
require "abstract_mapper"
|
@@ -79,21 +79,19 @@ module Faceter
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
-
# The node to define a renaming of
|
82
|
+
# The node to define a renaming of keys in a tuple
|
83
83
|
class Rename < AbstractMapper::Node
|
84
|
-
|
85
|
-
@key = key
|
86
|
-
@new_key = options.fetch(:to)
|
87
|
-
super
|
88
|
-
end
|
84
|
+
attribute :keys
|
89
85
|
|
90
86
|
def transproc
|
91
|
-
Transproc::HashTransformations[:rename_keys,
|
87
|
+
Transproc::HashTransformations[:rename_keys, keys]
|
92
88
|
end
|
93
89
|
end
|
94
90
|
end
|
95
91
|
```
|
96
92
|
|
93
|
+
[virtus]: https://github.com/solnic/virtus
|
94
|
+
|
97
95
|
### Define optimization rules
|
98
96
|
|
99
97
|
AbstractMapper defines 2 rules `AbstractMapper::SoleRule` and `AbstractMapper::PairRule`. The first one is applicable to every single node to check if it can be optimized by itself, the second one takes two consecutive nodes and either return them unchanged, or merges them into more time-efficient node.
|
@@ -125,29 +123,47 @@ module Faceter
|
|
125
123
|
# into the one list, containing subnodes (entries) from both sources.
|
126
124
|
class CompactLists < AbstractMapper::PairRule
|
127
125
|
def optimize?
|
128
|
-
nodes.
|
126
|
+
nodes.map { |n| n.is_a? List }.reduce(:&)
|
129
127
|
end
|
130
128
|
|
131
129
|
def optimize
|
132
130
|
List.new { nodes.map(:entries).flatten }
|
133
131
|
end
|
134
132
|
end
|
133
|
+
|
134
|
+
# Two consecutive renames can be merged
|
135
|
+
class CompactRenames < AbstractMapper::PairRule
|
136
|
+
def optimize?
|
137
|
+
nodes.map { |n| n.is_a? Rename }.reduce(:&)
|
138
|
+
end
|
139
|
+
|
140
|
+
def optimize
|
141
|
+
Rename.new nodes.map(&:attributes).reduce(:merge)
|
142
|
+
end
|
143
|
+
end
|
135
144
|
end
|
136
145
|
```
|
137
146
|
|
138
147
|
### Register commands and rules
|
139
148
|
|
140
|
-
Now that both the nodes (transformers) and optimization rules are defined, its time to register them for the mapper
|
149
|
+
Now that both the nodes (transformers) and optimization rules are defined, its time to register them for the mapper.
|
150
|
+
|
151
|
+
You can coerce command argumets into node attributes. The coercer is expected to return a hash:
|
141
152
|
|
142
153
|
```ruby
|
143
154
|
module Faceter
|
144
155
|
class Mapper < AbstractMapper
|
145
156
|
configure do
|
146
|
-
command :list,
|
147
|
-
|
157
|
+
command :list, List
|
158
|
+
|
159
|
+
# `:foo, to: :bar` becomes `{ keys: { foo: :bar } }`
|
160
|
+
command :rename, Rename do |name, opts|
|
161
|
+
{ keys: { name => opts.fetch(:to) } }
|
162
|
+
end
|
148
163
|
|
149
164
|
rule RemoveEmptyLists
|
150
165
|
rule CompactLists
|
166
|
+
rule CompactRenames
|
151
167
|
end
|
152
168
|
end
|
153
169
|
end
|
@@ -184,7 +200,7 @@ All the rules are applied before initializing `my_mapper`, so the AST will be th
|
|
184
200
|
|
185
201
|
```ruby
|
186
202
|
my_mapper.tree
|
187
|
-
# => <Root <List [<Rename(foo
|
203
|
+
# => <Root [<List [<Rename(foo: :bar, baz: :qux)>]>]>
|
188
204
|
```
|
189
205
|
|
190
206
|
Testing
|
data/Rakefile
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
6
|
-
exit
|
7
|
-
end
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "bundler/setup"
|
8
5
|
|
9
6
|
# Loads bundler tasks
|
10
7
|
Bundler::GemHelper.install_tasks
|
@@ -20,7 +17,7 @@ end
|
|
20
17
|
|
21
18
|
# Sets the Hexx::RSpec :test task to default
|
22
19
|
task :default do
|
23
|
-
system "bundle exec
|
20
|
+
system "bundle exec rake test:coverage:run"
|
24
21
|
end
|
25
22
|
|
26
23
|
desc "Runs mutation metric before the first evil being kept"
|
data/abstract_mapper.gemspec
CHANGED
@@ -19,8 +19,9 @@ Gem::Specification.new do |gem|
|
|
19
19
|
|
20
20
|
gem.required_ruby_version = ">= 2.1"
|
21
21
|
|
22
|
-
gem.add_runtime_dependency "
|
22
|
+
gem.add_runtime_dependency "ice_nine", "~> 0.11"
|
23
|
+
gem.add_runtime_dependency "transproc", "~> 0.3", ">= 0.3.1"
|
23
24
|
|
24
|
-
gem.add_development_dependency "hexx-rspec", "~> 0.
|
25
|
+
gem.add_development_dependency "hexx-rspec", "~> 0.5"
|
25
26
|
|
26
27
|
end # Gem::Specification
|
data/config/metrics/STYLEGUIDE
CHANGED
@@ -16,18 +16,17 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
16
16
|
- FEATURE - for adding new features, or backward-compatible changes;
|
17
17
|
- CHANGE - for backward-incompatible changes;
|
18
18
|
- BUG FIX - for fixing bugs;
|
19
|
-
-
|
20
|
-
- OTHER - for changes in documentaton, metrics etc, not touching the code;
|
19
|
+
- INTERNAL - for other changes of the code, documentaton, metrics etc.;
|
21
20
|
- VERSION - for version changes.
|
22
21
|
|
23
|
-
* Always separate commits of different types (such as FEATURE and
|
22
|
+
* Always separate commits of different types (such as FEATURE and INTERNAL).
|
24
23
|
|
25
|
-
*
|
24
|
+
* Separate various features from each other.
|
26
25
|
|
27
26
|
* Include specification to the same commit as the code.
|
28
27
|
|
29
28
|
* Run all tests before making a commit.
|
30
|
-
Never commit the code that break
|
29
|
+
Never commit the code that break a specification.
|
31
30
|
|
32
31
|
* Use metric (run `rake check`) before making a commit.
|
33
32
|
|
@@ -37,9 +36,9 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
37
36
|
|
38
37
|
http://semver.org/
|
39
38
|
|
40
|
-
* For versions name the commit after a version number
|
39
|
+
* For versions name the commit after a version number:
|
41
40
|
|
42
|
-
VERSION 1.0.0-rc2
|
41
|
+
[VERSION] 1.0.0-rc2
|
43
42
|
|
44
43
|
|
45
44
|
== Formatting:
|
@@ -71,7 +70,7 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
71
70
|
|
72
71
|
== Syntax:
|
73
72
|
|
74
|
-
* Write for
|
73
|
+
* Write for 1.9.3+
|
75
74
|
|
76
75
|
* Use double quotes
|
77
76
|
|
@@ -89,14 +88,13 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
89
88
|
of thumb: If you have to use outer parentheses, you are using the
|
90
89
|
wrong operators.)
|
91
90
|
|
92
|
-
* Avoid double negation (!!), unless Null
|
93
|
-
|
91
|
+
* Avoid double negation (!!), unless Null Object is expected.
|
92
|
+
|
94
93
|
http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness
|
95
94
|
|
96
95
|
* Avoid multiline ?:, use if.
|
97
96
|
|
98
|
-
* Use {...} when defining blocks on one line. Use do...end for multiline
|
99
|
-
blocks.
|
97
|
+
* Use {...} when defining blocks on one line. Use do...end for multiline blocks.
|
100
98
|
|
101
99
|
* Avoid return where not required.
|
102
100
|
|
@@ -133,7 +131,8 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
133
131
|
|
134
132
|
* Prefer map over collect, detect over find, select over find_all.
|
135
133
|
|
136
|
-
* Use def self.method to define singleton methods.
|
134
|
+
* Use def self.method to define singleton methods. Extend modules
|
135
|
+
when you need many singleton methods.
|
137
136
|
|
138
137
|
* Avoid alias when alias_method will do.
|
139
138
|
|
@@ -141,9 +140,9 @@ https://github.com/dkubb/styleguide/blob/master/RUBY-STYLE
|
|
141
140
|
== Comments:
|
142
141
|
|
143
142
|
* Use YARD and its conventions for API documentation. Don't put an
|
144
|
-
empty line between the comment block and the def.
|
143
|
+
empty line between the comment block and the def. Use empty comment line.
|
145
144
|
|
146
|
-
*
|
145
|
+
* Capitalize comments longer than a word, and use punctuation.
|
147
146
|
Use one space after periods.
|
148
147
|
|
149
148
|
* Avoid superfluous comments.
|
data/lib/abstract_mapper.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require "ice_nine"
|
3
4
|
require "transproc"
|
4
5
|
|
5
6
|
require_relative "abstract_mapper/functions"
|
@@ -8,8 +9,10 @@ require_relative "abstract_mapper/errors/unknown_command"
|
|
8
9
|
require_relative "abstract_mapper/errors/wrong_node"
|
9
10
|
require_relative "abstract_mapper/errors/wrong_rule"
|
10
11
|
|
12
|
+
require_relative "abstract_mapper/attributes"
|
11
13
|
require_relative "abstract_mapper/node"
|
12
14
|
require_relative "abstract_mapper/branch"
|
15
|
+
require_relative "abstract_mapper/command"
|
13
16
|
require_relative "abstract_mapper/commands"
|
14
17
|
require_relative "abstract_mapper/builder"
|
15
18
|
|
@@ -65,7 +68,7 @@ class AbstractMapper
|
|
65
68
|
def initialize
|
66
69
|
@tree = self.class.finalize
|
67
70
|
@transproc = @tree.transproc
|
68
|
-
|
71
|
+
IceNine.deep_freeze(self)
|
69
72
|
end
|
70
73
|
|
71
74
|
# Maps the input data to some output using the transformation,
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class AbstractMapper
|
4
|
+
|
5
|
+
# Defines attributes along with the DSL for setting them
|
6
|
+
#
|
7
|
+
module Attributes
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def self.included(klass)
|
11
|
+
klass.__send__ :extend, DSL
|
12
|
+
end
|
13
|
+
|
14
|
+
# @!attribute [r] attributes
|
15
|
+
#
|
16
|
+
# @return [Hash] The initialized attributes
|
17
|
+
#
|
18
|
+
attr_reader :attributes
|
19
|
+
|
20
|
+
# Initializes the instance with the hash of attributes
|
21
|
+
#
|
22
|
+
# @param [Hash] attributes
|
23
|
+
#
|
24
|
+
def initialize(attributes)
|
25
|
+
@attributes = Functions[:restrict, self.class.attributes][attributes]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Attributes DSL
|
29
|
+
#
|
30
|
+
module DSL
|
31
|
+
|
32
|
+
# Makes a attributes inheritable
|
33
|
+
#
|
34
|
+
# @private
|
35
|
+
#
|
36
|
+
def inherited(klass)
|
37
|
+
attributes.each { |key, value| klass.attribute key, default: value }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Default attributes for the node
|
41
|
+
#
|
42
|
+
# @return [Hash]
|
43
|
+
#
|
44
|
+
def attributes
|
45
|
+
@attributes ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Declares the attribute
|
49
|
+
#
|
50
|
+
# @param [#to_sym] name
|
51
|
+
# @param [Hash] options
|
52
|
+
# @option options [Object] :default
|
53
|
+
#
|
54
|
+
# @return [undefined]
|
55
|
+
#
|
56
|
+
def attribute(name, options = {})
|
57
|
+
attributes[name.to_sym] = options[:default]
|
58
|
+
define_method(name) { attributes[name.to_sym] }
|
59
|
+
end
|
60
|
+
|
61
|
+
end # module DSL
|
62
|
+
|
63
|
+
end # module Attributes
|
64
|
+
|
65
|
+
end # class AbstractMapper
|
@@ -6,7 +6,7 @@ class AbstractMapper
|
|
6
6
|
# applied to some level of nested input.
|
7
7
|
#
|
8
8
|
# Unlike the simple node, describing a transformation of data, the
|
9
|
-
# branch carries a collection of subnodes along with methods to [#
|
9
|
+
# branch carries a collection of subnodes along with methods to [#update]
|
10
10
|
# itself with the same attributes and different subnodes.
|
11
11
|
#
|
12
12
|
# Tne branch only stores subnodes and composes transformations.
|
@@ -32,9 +32,9 @@ class AbstractMapper
|
|
32
32
|
# @return [Branch::Node]
|
33
33
|
|
34
34
|
# @private
|
35
|
-
def initialize(
|
35
|
+
def initialize(attributes = {})
|
36
36
|
@subnodes = block_given? ? yield : []
|
37
|
-
super(
|
37
|
+
super(attributes, &nil)
|
38
38
|
end
|
39
39
|
|
40
40
|
# Returns a new branch of the same type, with the same attributes,
|
@@ -43,7 +43,7 @@ class AbstractMapper
|
|
43
43
|
# @example
|
44
44
|
# branch = Branch.new(:foo)
|
45
45
|
# # => <Branch(:foo) []>
|
46
|
-
# branch.
|
46
|
+
# branch.update { Node.new(:bar) }
|
47
47
|
# # => <Branch(:foo) [<Node(:bar)>]>
|
48
48
|
#
|
49
49
|
# @param [Proc] block
|
@@ -53,8 +53,8 @@ class AbstractMapper
|
|
53
53
|
#
|
54
54
|
# @yield block
|
55
55
|
#
|
56
|
-
def
|
57
|
-
self.class.new(
|
56
|
+
def update
|
57
|
+
self.class.new(attributes) { yield }
|
58
58
|
end
|
59
59
|
|
60
60
|
# @!method each
|
@@ -73,7 +73,7 @@ class AbstractMapper
|
|
73
73
|
# @return [AbstractMapper::Branch]
|
74
74
|
#
|
75
75
|
def <<(other)
|
76
|
-
|
76
|
+
update { entries << other }
|
77
77
|
end
|
78
78
|
|
79
79
|
# The composition of transformations from all subnodes of the branch
|
@@ -97,6 +97,16 @@ class AbstractMapper
|
|
97
97
|
"#{super} [#{map(&:inspect).join(", ")}]"
|
98
98
|
end
|
99
99
|
|
100
|
+
# Checks equality of branches by type, attributes and subnodes
|
101
|
+
#
|
102
|
+
# @param [Other] other
|
103
|
+
#
|
104
|
+
# @return [Boolean]
|
105
|
+
#
|
106
|
+
def eql?(other)
|
107
|
+
super && entries.eql?(other.entries)
|
108
|
+
end
|
109
|
+
|
100
110
|
private
|
101
111
|
|
102
112
|
# Substitutes the name of the class by the special name "Root"
|
@@ -22,7 +22,11 @@ class AbstractMapper
|
|
22
22
|
#
|
23
23
|
# @return [AbstractMapper::Commands] The registry of DSL commands
|
24
24
|
#
|
25
|
-
|
25
|
+
attr_writer :commands
|
26
|
+
|
27
|
+
def commands
|
28
|
+
@commands ||= AbstractMapper::Commands
|
29
|
+
end
|
26
30
|
|
27
31
|
end # eigenclass
|
28
32
|
|
@@ -61,13 +65,13 @@ class AbstractMapper
|
|
61
65
|
@tree = node
|
62
66
|
@commands = self.class.commands
|
63
67
|
instance_eval(&block) if block_given?
|
64
|
-
|
68
|
+
IceNine.deep_freeze(self)
|
65
69
|
end
|
66
70
|
|
67
71
|
private # DSL commands
|
68
72
|
|
69
73
|
def method_missing(name, *args, &block)
|
70
|
-
node = @commands[name
|
74
|
+
node = @commands[name].call(*args, &block)
|
71
75
|
@tree = tree << (node.is_a?(Branch) ? update(node, &block) : node)
|
72
76
|
end
|
73
77
|
|