ardm-sweatshop 1.2.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 +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +11 -0
- data/Gemfile +52 -0
- data/LICENSE +20 -0
- data/README.rdoc +259 -0
- data/Rakefile +4 -0
- data/ardm-sweatshop.gemspec +25 -0
- data/lib/ardm-sweatshop.rb +1 -0
- data/lib/dm-sweatshop.rb +6 -0
- data/lib/dm-sweatshop/model.rb +107 -0
- data/lib/dm-sweatshop/support/class_attributes.rb +46 -0
- data/lib/dm-sweatshop/sweatshop.rb +145 -0
- data/lib/dm-sweatshop/unique.rb +89 -0
- data/lib/dm-sweatshop/version.rb +5 -0
- data/spec/dm-sweatshop/model_spec.rb +209 -0
- data/spec/dm-sweatshop/sweatshop_spec.rb +166 -0
- data/spec/dm-sweatshop/unique_spec.rb +84 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +17 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +134 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module DataMapper
|
|
2
|
+
class Sweatshop
|
|
3
|
+
module ClassAttributes
|
|
4
|
+
def self.reader(klass, *attributes)
|
|
5
|
+
attributes.each do |attribute|
|
|
6
|
+
klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
|
7
|
+
unless defined? @@#{attribute}
|
|
8
|
+
@@#{attribute} = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.#{attribute}
|
|
12
|
+
@@#{attribute}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def #{attribute}
|
|
16
|
+
@@#{attribute}
|
|
17
|
+
end
|
|
18
|
+
RUBY
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.writer(klass, *attributes)
|
|
23
|
+
attributes.each do |attribute|
|
|
24
|
+
klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
|
25
|
+
unless defined? @@#{attribute}
|
|
26
|
+
@@#{attribute} = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.#{attribute}=(obj)
|
|
30
|
+
@@#{attribute} = obj
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def #{attribute}=(obj)
|
|
34
|
+
@@#{attribute} = obj
|
|
35
|
+
end
|
|
36
|
+
RUBY
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.accessor(klass, *attributes)
|
|
41
|
+
self.reader(klass, *attributes)
|
|
42
|
+
self.writer(klass, *attributes)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module DataMapper
|
|
2
|
+
class Sweatshop
|
|
3
|
+
# Raise when requested attributes hash or instance are not
|
|
4
|
+
# found in model and record maps, respectively.
|
|
5
|
+
#
|
|
6
|
+
# This usually happens when you forget to use +make+ or
|
|
7
|
+
# +generate+ method before trying to +pick+ an object.
|
|
8
|
+
class NoFixtureExist < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_accessor :model_map
|
|
13
|
+
attr_accessor :record_map
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Models map stores named Procs for a class.
|
|
17
|
+
# Each Proc must return a Hash of attributes.
|
|
18
|
+
self.model_map = Hash.new {|ha,ka| ha[ka] = Hash.new {|hb,kb| hb[kb] = []}}
|
|
19
|
+
# Records map stores named instances of a class.
|
|
20
|
+
# Those instances may or may not be new records.
|
|
21
|
+
self.record_map = Hash.new {|ha,ka| ha[ka] = Hash.new {|hb,kb| hb[kb] = []}}
|
|
22
|
+
|
|
23
|
+
# Adds a Proc to model map. Proc must return a Hash of attributes.
|
|
24
|
+
#
|
|
25
|
+
# @param klass [Class, DataMapper::Resource]
|
|
26
|
+
# @param name [Symbol]
|
|
27
|
+
# @param instance [DataMapper::Resource]
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
# @return [Array] model map
|
|
32
|
+
def self.add(klass, name, &proc)
|
|
33
|
+
self.model_map[klass][name.to_sym] << proc
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Adds an instance to records map.
|
|
37
|
+
#
|
|
38
|
+
# @param klass [Class, DataMapper::Resource]
|
|
39
|
+
# @param name [Symbol]
|
|
40
|
+
# @param instance [DataMapper::Resource]
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
#
|
|
44
|
+
# @return [DataMapper::Resource] added instance
|
|
45
|
+
def self.record(klass, name, instance)
|
|
46
|
+
self.record_map[klass][name.to_sym] << instance
|
|
47
|
+
instance
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Same as create but calls Model#create! and does save
|
|
51
|
+
# invalid models
|
|
52
|
+
#
|
|
53
|
+
# @param klass [Class, DataMapper::Resource]
|
|
54
|
+
# @param name [Symbol]
|
|
55
|
+
# @param attributes [Hash]
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
#
|
|
59
|
+
# @return [DataMapper::Resource] added instance
|
|
60
|
+
def self.create!(klass, name, attributes = {})
|
|
61
|
+
record(klass, name, klass.create!(attributes(klass, name).merge(attributes)))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Creates an instance from given hash of attributes, saves it
|
|
65
|
+
# and adds it to the record map.
|
|
66
|
+
#
|
|
67
|
+
# @param klass [Class, DataMapper::Resource]
|
|
68
|
+
# @param name [Symbol]
|
|
69
|
+
# @param attributes [Hash]
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
#
|
|
73
|
+
# @return [DataMapper::Resource] added instance
|
|
74
|
+
def self.create(klass, name, attributes = {})
|
|
75
|
+
record(klass, name, klass.create(attributes(klass, name).merge(attributes)))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Creates an instance from given hash of attributes
|
|
79
|
+
# and adds it to records map without saving.
|
|
80
|
+
#
|
|
81
|
+
# @param klass [Class, DataMapper::Resource]
|
|
82
|
+
# @param name [Symbol]
|
|
83
|
+
# @param attributes [Hash]
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
#
|
|
87
|
+
# @return [DataMapper::Resource] added instance
|
|
88
|
+
def self.make(klass, name, attributes = {})
|
|
89
|
+
record(klass, name, klass.new(attributes(klass, name).merge(attributes)))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns a pre existing instance of a model from the record map
|
|
93
|
+
#
|
|
94
|
+
# @param klass [Class, DataMapper::Resource]
|
|
95
|
+
# @param name [Symbol]
|
|
96
|
+
#
|
|
97
|
+
# @return [DataMapper::Resource] existing instance of a model from the record map
|
|
98
|
+
# @raise DataMapper::Sweatshop::NoFixtureExist when requested fixture does not exist in the record map
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
def self.pick(klass, name)
|
|
102
|
+
self.record_map[klass][name.to_sym].pick || raise(NoFixtureExist, "no #{name} context fixtures have been generated for the #{klass} class")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns a Hash of attributes from the model map
|
|
106
|
+
#
|
|
107
|
+
# @param klass [Class, DataMapper::Resource]
|
|
108
|
+
# @param name [Symbol]
|
|
109
|
+
#
|
|
110
|
+
# @return [Hash] existing instance of a model from the model map
|
|
111
|
+
# @raise NoFixtureExist when requested fixture does not exist in the model map
|
|
112
|
+
#
|
|
113
|
+
# @api private
|
|
114
|
+
def self.attributes(klass, name)
|
|
115
|
+
proc = model_map[klass][name.to_sym].pick
|
|
116
|
+
|
|
117
|
+
if proc
|
|
118
|
+
expand_callable_values(proc.call)
|
|
119
|
+
elsif klass.superclass.is_a?(DataMapper::Model)
|
|
120
|
+
attributes(klass.superclass, name)
|
|
121
|
+
else
|
|
122
|
+
raise NoFixtureExist, "#{name} fixture was not found for class #{klass}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Returns a Hash with callable values evaluated.
|
|
127
|
+
#
|
|
128
|
+
# @param hash [Hash]
|
|
129
|
+
#
|
|
130
|
+
# @return [Hash] existing instance of a model from the model map
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
def self.expand_callable_values(hash)
|
|
134
|
+
expanded = {}
|
|
135
|
+
hash.each do |key, value|
|
|
136
|
+
if value.respond_to?(:call)
|
|
137
|
+
expanded[key] = value.call
|
|
138
|
+
else
|
|
139
|
+
expanded[key] = value
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
expanded
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'dm-sweatshop/support/class_attributes'
|
|
2
|
+
|
|
3
|
+
module DataMapper
|
|
4
|
+
class Sweatshop
|
|
5
|
+
module Unique
|
|
6
|
+
# Yields a value to the block. The value is unique for each invocation
|
|
7
|
+
# with the same block. Alternatively, you may provide an explicit key to
|
|
8
|
+
# identify the block.
|
|
9
|
+
#
|
|
10
|
+
# If a block with no parameter is supplied, unique keeps track of previous
|
|
11
|
+
# invocations, and will continue yielding until a unique value is generated.
|
|
12
|
+
# If a unique value is not generated after @UniqueWorker::MAX_TRIES@, an exception
|
|
13
|
+
# is raised.
|
|
14
|
+
#
|
|
15
|
+
# ParseTree is required unless an explicit key is provided
|
|
16
|
+
#
|
|
17
|
+
# (1..3).collect { unique {|x| x }} # => [0, 1, 2]
|
|
18
|
+
# (1..3).collect { unique {|x| x + 1 }} # => [1, 2, 3]
|
|
19
|
+
# (1..3).collect { unique {|x| x }} # => [3, 4, 5] # Continued on from above
|
|
20
|
+
# (1..3).collect { unique(:a) {|x| x }} # => [0, 1, 2] # Explicit key overrides block identity
|
|
21
|
+
#
|
|
22
|
+
# a = [1, 1, 1, 2, 2, 3]
|
|
23
|
+
# (1..3).collect { unique { a.shift }} # => [1, 2, 3]
|
|
24
|
+
# (1..3).collect { unique { 1 }} # raises TooManyTriesException
|
|
25
|
+
#
|
|
26
|
+
# return <Object> the return value of the block
|
|
27
|
+
def unique(key = nil, &block)
|
|
28
|
+
if block.arity < 1
|
|
29
|
+
UniqueWorker.unique_map ||= {}
|
|
30
|
+
|
|
31
|
+
key ||= UniqueWorker.key_for(&block)
|
|
32
|
+
set = UniqueWorker.unique_map[key] || Set.new
|
|
33
|
+
result = block[]
|
|
34
|
+
tries = 0
|
|
35
|
+
while set.include?(result)
|
|
36
|
+
result = block[]
|
|
37
|
+
tries += 1
|
|
38
|
+
|
|
39
|
+
raise TooManyTriesException.new("Could not generate unique value after #{tries} attempts") if tries >= UniqueWorker::MAX_TRIES
|
|
40
|
+
end
|
|
41
|
+
set << result
|
|
42
|
+
UniqueWorker.unique_map[key] = set
|
|
43
|
+
else
|
|
44
|
+
UniqueWorker.count_map ||= Hash.new() { 0 }
|
|
45
|
+
|
|
46
|
+
key ||= UniqueWorker.key_for(&block)
|
|
47
|
+
result = block[UniqueWorker.count_map[key]]
|
|
48
|
+
UniqueWorker.count_map[key] += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class TooManyTriesException < RuntimeError; end;
|
|
55
|
+
end
|
|
56
|
+
extend(Unique)
|
|
57
|
+
|
|
58
|
+
class UniqueWorker
|
|
59
|
+
MAX_TRIES = 10
|
|
60
|
+
|
|
61
|
+
unless defined?(JRUBY_VERSION) || RUBY_VERSION >= '1.9'
|
|
62
|
+
begin
|
|
63
|
+
gem 'ParseTree', '~>3.0.3'
|
|
64
|
+
require 'parse_tree'
|
|
65
|
+
rescue LoadError
|
|
66
|
+
puts 'DataMapper::Sweatshop::Unique - ParseTree could not be loaded, anonymous uniques will not be allowed'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
ClassAttributes.accessor(self, :count_map)
|
|
71
|
+
ClassAttributes.accessor(self, :unique_map)
|
|
72
|
+
ClassAttributes.accessor(self, :parser)
|
|
73
|
+
|
|
74
|
+
# Use the sexp representation of the block as a unique key for the block
|
|
75
|
+
# If you copy and paste a block, it will still have the same key
|
|
76
|
+
#
|
|
77
|
+
# return <Object> the unique key for the block
|
|
78
|
+
def self.key_for(&block)
|
|
79
|
+
raise "You need to install ParseTree to use anonymous an anonymous unique (gem install ParseTree). In the mean time, explicitly declare a key: unique(:my_key) { ... }" unless Object::const_defined?("ParseTree")
|
|
80
|
+
|
|
81
|
+
klass = Class.new
|
|
82
|
+
name = "tmp"
|
|
83
|
+
klass.send(:define_method, name, &block)
|
|
84
|
+
self.parser ||= ParseTree.new(false)
|
|
85
|
+
self.parser.parse_tree_for_method(klass, name).last
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe DataMapper::Model do
|
|
4
|
+
|
|
5
|
+
class Widget
|
|
6
|
+
include DataMapper::Resource
|
|
7
|
+
property :id, Serial
|
|
8
|
+
property :type, Discriminator
|
|
9
|
+
property :name, String
|
|
10
|
+
property :price, Integer
|
|
11
|
+
|
|
12
|
+
belongs_to :order, :required => false
|
|
13
|
+
validates_presence_of :price
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Wonket < Widget
|
|
17
|
+
property :size, String
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Order
|
|
21
|
+
include DataMapper::Resource
|
|
22
|
+
|
|
23
|
+
property :id, Serial
|
|
24
|
+
|
|
25
|
+
has n, :widgets
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
before(:each) do
|
|
29
|
+
DataMapper.auto_migrate!
|
|
30
|
+
DataMapper::Sweatshop.model_map.clear
|
|
31
|
+
DataMapper::Sweatshop.record_map.clear
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
supported_by :all do
|
|
35
|
+
|
|
36
|
+
describe ".default_fauxture_name" do
|
|
37
|
+
it "is :default" do
|
|
38
|
+
Order.default_fauxture_name.should == :default
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe ".fixture" do
|
|
43
|
+
describe "without fauxture name" do
|
|
44
|
+
before :each do
|
|
45
|
+
Widget.fixture {{
|
|
46
|
+
:name => /\w+/.gen.capitalize,
|
|
47
|
+
:price => /\d{4,5}/.gen.to_i
|
|
48
|
+
}}
|
|
49
|
+
|
|
50
|
+
@default = DataMapper::Sweatshop.model_map[Widget][:default]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "add a fixture proc for the model with name :default" do
|
|
54
|
+
@default.should_not be_empty
|
|
55
|
+
@default.first.should be_kind_of(Proc)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should allow handle complex named fixtures" do
|
|
60
|
+
Wonket.fix {{
|
|
61
|
+
:name => /\w+ Wonket/.gen.capitalize,
|
|
62
|
+
:price => /\d{2,3}99/.gen.to_i,
|
|
63
|
+
:size => %w[small medium large xl].pick
|
|
64
|
+
}}
|
|
65
|
+
|
|
66
|
+
Order.fix {{
|
|
67
|
+
:widgets => (1..5).of { Widget.gen }
|
|
68
|
+
}}
|
|
69
|
+
|
|
70
|
+
Order.fix(:wonket_order) {{
|
|
71
|
+
:widgets => (5..10).of { Wonket.gen }
|
|
72
|
+
}}
|
|
73
|
+
|
|
74
|
+
wonket_order = Order.gen(:wonket_order)
|
|
75
|
+
wonket_order.widgets.should_not be_empty
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "should allow for STI fixtures" do
|
|
79
|
+
Widget.fix {{
|
|
80
|
+
:name => /\w+/.gen.capitalize,
|
|
81
|
+
:price => /\d{4,5}/.gen.to_i
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
Order.fix {{
|
|
85
|
+
:widgets => (1..5).of { Wonket.gen }
|
|
86
|
+
}}
|
|
87
|
+
|
|
88
|
+
Order.gen.widgets.should_not be_empty
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe ".make" do
|
|
93
|
+
before :each do
|
|
94
|
+
Widget.fix(:red) {{
|
|
95
|
+
:name => "red",
|
|
96
|
+
:price => 20
|
|
97
|
+
}}
|
|
98
|
+
|
|
99
|
+
@widget = Widget.make(:red)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "creates an object from named attributes hash" do
|
|
103
|
+
@widget.name.should == "red"
|
|
104
|
+
@widget.price.should == 20
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "returns a new object" do
|
|
108
|
+
@widget.should be_new
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe ".generate" do
|
|
113
|
+
before :each do
|
|
114
|
+
Widget.fix(:red) {{
|
|
115
|
+
:name => "red",
|
|
116
|
+
:price => 20
|
|
117
|
+
}}
|
|
118
|
+
|
|
119
|
+
@widget = Widget.gen(:red)
|
|
120
|
+
|
|
121
|
+
Widget.fix(:blue) {{
|
|
122
|
+
:name => "blue"
|
|
123
|
+
}}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "creates an object from named attributes hash" do
|
|
127
|
+
@widget.name.should == "red"
|
|
128
|
+
@widget.price.should == 20
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "returns a saved object" do
|
|
132
|
+
@widget.should be_saved
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "does not save invalid model" do
|
|
136
|
+
blue_widget = Widget.gen(:blue)
|
|
137
|
+
blue_widget.should be_new
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe ".generate!" do
|
|
142
|
+
it "saves a model even if it is invalid" do
|
|
143
|
+
Widget.fix(:blue) {{
|
|
144
|
+
:name => "blue"
|
|
145
|
+
}}
|
|
146
|
+
|
|
147
|
+
blue_widget = Widget.gen!(:blue)
|
|
148
|
+
blue_widget.should be_saved
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe ".pick" do
|
|
153
|
+
before :each do
|
|
154
|
+
Widget.fix(:red) {{
|
|
155
|
+
:name => "rosso",
|
|
156
|
+
:price => 20
|
|
157
|
+
}}
|
|
158
|
+
|
|
159
|
+
Widget.fix(:yellow) {{
|
|
160
|
+
:name => "giallo",
|
|
161
|
+
:price => 30
|
|
162
|
+
}}
|
|
163
|
+
|
|
164
|
+
Widget.fix(:blue) {{
|
|
165
|
+
:name => Proc.new { "b" + "lu" },
|
|
166
|
+
:price => 40
|
|
167
|
+
}}
|
|
168
|
+
|
|
169
|
+
@red = Widget.gen(:red)
|
|
170
|
+
@yellow = Widget.gen(:yellow)
|
|
171
|
+
@blue = Widget.gen(:blue)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "returns a pre existing object with named attributes hash" do
|
|
175
|
+
@red.name.should == "rosso"
|
|
176
|
+
@red.price.should == 20
|
|
177
|
+
|
|
178
|
+
@yellow.name.should == "giallo"
|
|
179
|
+
@yellow.price.should == 30
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "expands callable values of attributes hash" do
|
|
183
|
+
@blue.name.should == "blu"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe ".generate_attributes" do
|
|
188
|
+
before :each do
|
|
189
|
+
Widget.fix(:red) {{
|
|
190
|
+
:name => "red",
|
|
191
|
+
:price => 20
|
|
192
|
+
}}
|
|
193
|
+
|
|
194
|
+
@hash = Widget.generate_attributes(:red)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "returns a Hash" do
|
|
198
|
+
@hash.should be_an_instance_of(Hash)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "returns stored attributes hash by name" do
|
|
202
|
+
@hash[:name].should == "red"
|
|
203
|
+
@hash[:price].should == 20
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
end
|