miasma 0.0.1 → 0.1.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/CHANGELOG.md +2 -0
- data/README.md +179 -0
- data/lib/miasma.rb +52 -0
- data/lib/miasma/contrib/aws.rb +390 -0
- data/lib/miasma/contrib/aws/auto_scale.rb +85 -0
- data/lib/miasma/contrib/aws/compute.rb +112 -0
- data/lib/miasma/contrib/aws/load_balancer.rb +185 -0
- data/lib/miasma/contrib/aws/orchestration.rb +338 -0
- data/lib/miasma/contrib/rackspace.rb +164 -0
- data/lib/miasma/contrib/rackspace/auto_scale.rb +84 -0
- data/lib/miasma/contrib/rackspace/compute.rb +104 -0
- data/lib/miasma/contrib/rackspace/load_balancer.rb +117 -0
- data/lib/miasma/contrib/rackspace/orchestration.rb +255 -0
- data/lib/miasma/error.rb +89 -0
- data/lib/miasma/models.rb +14 -0
- data/lib/miasma/models/auto_scale.rb +55 -0
- data/lib/miasma/models/auto_scale/group.rb +64 -0
- data/lib/miasma/models/auto_scale/groups.rb +34 -0
- data/lib/miasma/models/block_storage.rb +0 -0
- data/lib/miasma/models/compute.rb +70 -0
- data/lib/miasma/models/compute/server.rb +71 -0
- data/lib/miasma/models/compute/servers.rb +35 -0
- data/lib/miasma/models/dns.rb +0 -0
- data/lib/miasma/models/load_balancer.rb +55 -0
- data/lib/miasma/models/load_balancer/balancer.rb +72 -0
- data/lib/miasma/models/load_balancer/balancers.rb +34 -0
- data/lib/miasma/models/monitoring.rb +0 -0
- data/lib/miasma/models/orchestration.rb +127 -0
- data/lib/miasma/models/orchestration/event.rb +38 -0
- data/lib/miasma/models/orchestration/events.rb +64 -0
- data/lib/miasma/models/orchestration/resource.rb +79 -0
- data/lib/miasma/models/orchestration/resources.rb +55 -0
- data/lib/miasma/models/orchestration/stack.rb +144 -0
- data/lib/miasma/models/orchestration/stacks.rb +46 -0
- data/lib/miasma/models/queues.rb +0 -0
- data/lib/miasma/models/storage.rb +60 -0
- data/lib/miasma/models/storage/bucket.rb +36 -0
- data/lib/miasma/models/storage/buckets.rb +41 -0
- data/lib/miasma/models/storage/file.rb +45 -0
- data/lib/miasma/models/storage/files.rb +52 -0
- data/lib/miasma/types.rb +13 -0
- data/lib/miasma/types/api.rb +145 -0
- data/lib/miasma/types/collection.rb +116 -0
- data/lib/miasma/types/data.rb +53 -0
- data/lib/miasma/types/model.rb +118 -0
- data/lib/miasma/types/thin_model.rb +76 -0
- data/lib/miasma/utils.rb +12 -0
- data/lib/miasma/utils/animal_strings.rb +29 -0
- data/lib/miasma/utils/immutable.rb +36 -0
- data/lib/miasma/utils/lazy.rb +231 -0
- data/lib/miasma/utils/memoization.rb +55 -0
- data/lib/miasma/utils/smash.rb +149 -0
- data/lib/miasma/version.rb +4 -0
- data/miasma.gemspec +18 -0
- metadata +57 -3
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Base data container
|
7
|
+
class Data
|
8
|
+
|
9
|
+
include Miasma::Utils::Lazy
|
10
|
+
|
11
|
+
attribute :id, [String, Numeric]
|
12
|
+
|
13
|
+
# Build new data instance
|
14
|
+
#
|
15
|
+
# @param args [Hash] attribute values
|
16
|
+
# @return [self]
|
17
|
+
def initialize(args={})
|
18
|
+
load_data(args)
|
19
|
+
valid_state
|
20
|
+
end
|
21
|
+
|
22
|
+
# Convert model to JSON string
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def to_json(*_)
|
26
|
+
MultiJson.dump(attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Load model using JSON string
|
30
|
+
#
|
31
|
+
# @param json [String]
|
32
|
+
# @return [self]
|
33
|
+
def from_json(json)
|
34
|
+
load_data(
|
35
|
+
MultiJson.load(json).to_smash
|
36
|
+
).valid_state
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
|
41
|
+
# Build new instance from JSON string
|
42
|
+
#
|
43
|
+
# @param json [String]
|
44
|
+
# @return [Data]
|
45
|
+
def from_json(json)
|
46
|
+
self.new(MultiJson.load(json).to_smash)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Base model
|
7
|
+
class Model < Data
|
8
|
+
|
9
|
+
# @return [Miasma::Types::Api] underlying service API
|
10
|
+
attr_reader :api
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
# Build new model from JSON
|
15
|
+
#
|
16
|
+
# @param api [Miasma::Types::Api]
|
17
|
+
# @param json [String]
|
18
|
+
# @return [Model]
|
19
|
+
def from_json(api, json)
|
20
|
+
instance = self.new(api)
|
21
|
+
instance.from_json(json)
|
22
|
+
instance
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# Build new model
|
28
|
+
#
|
29
|
+
# @param api [Miasma::Types::Api] service API
|
30
|
+
# @param model_data [Smash] load model data if provided
|
31
|
+
# @return [self]
|
32
|
+
def initialize(api, model_data=nil)
|
33
|
+
@api = api
|
34
|
+
@data = Smash.new
|
35
|
+
@dirty = Smash.new
|
36
|
+
if(model_data)
|
37
|
+
if(model_data.is_a?(Hash))
|
38
|
+
load_data(model_data)
|
39
|
+
else
|
40
|
+
raise TypeError.new "Expecting `model_data` to be of type `Hash`. Received: `#{model_data.class}`"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Save changes to the model
|
46
|
+
#
|
47
|
+
# @return [TrueClass, FalseClass] save was performed
|
48
|
+
# @raises [Miasma::Error::Save]
|
49
|
+
def save
|
50
|
+
if(dirty?)
|
51
|
+
perform_save
|
52
|
+
reload
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Destroy the model
|
59
|
+
#
|
60
|
+
# @return [TrueClass, FalseClass] destruction was performed
|
61
|
+
# @raises [Miasma::Error::Destroy]
|
62
|
+
def destroy
|
63
|
+
if(persisted?)
|
64
|
+
perform_destroy
|
65
|
+
reload
|
66
|
+
true
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reload the underlying data for model
|
73
|
+
#
|
74
|
+
# @return [self]
|
75
|
+
def reload
|
76
|
+
perform_reload
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [TrueClass, FalseClass] model is persisted
|
81
|
+
def persisted?
|
82
|
+
id?
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [String, Integer]
|
86
|
+
def id?
|
87
|
+
data[:id] || dirty[:id]
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Save model state to remote API
|
93
|
+
#
|
94
|
+
# @return [TrueClass, FalseClass] performed remote action
|
95
|
+
# @raises [Miasma::Error::Save]
|
96
|
+
def perform_save
|
97
|
+
raise NotImplementedError.new 'Remote API save has not been implemented'
|
98
|
+
end
|
99
|
+
|
100
|
+
# Reload model state from remote API
|
101
|
+
#
|
102
|
+
# @return [TrueClass, FalseClass] performed remote action
|
103
|
+
# @raises [Miasma::Error::Save]
|
104
|
+
def perform_reload
|
105
|
+
raise NotImplementedError.new 'Remote API reload has not been implemented'
|
106
|
+
end
|
107
|
+
|
108
|
+
# Destroy model from remote API
|
109
|
+
#
|
110
|
+
# @return [TrueClass, FalseClass] performed remote action
|
111
|
+
# @raises [Miasma::Error::Save]
|
112
|
+
def perform_destroy
|
113
|
+
raise NotImplementedError.new 'Remote API destroy has not been implemented'
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Base data container
|
7
|
+
class ThinModel < Data
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
# Get/Set fat model
|
12
|
+
#
|
13
|
+
# @param klass [Class] fat model class
|
14
|
+
# @return [Class] fat model class
|
15
|
+
def model(klass=nil)
|
16
|
+
if(klass)
|
17
|
+
unless(klass.ancestors.include?(Miasma::Types::Model))
|
18
|
+
raise TypeError.new "Expecting `Miasma::Types::Model` subclass! (got #{klass})"
|
19
|
+
else
|
20
|
+
self._model = klass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
self._model
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
# @return [Class] fat model class
|
29
|
+
attr_accessor :_model
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Miasma::Types::Api] service API
|
34
|
+
attr_reader :api
|
35
|
+
|
36
|
+
# Build new instance
|
37
|
+
#
|
38
|
+
# @param api [Miasma::Types::Api] service API
|
39
|
+
# @param args [Hash] model data
|
40
|
+
def initialize(api, args={})
|
41
|
+
@api = api
|
42
|
+
super args
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [FalseClass]
|
46
|
+
# @note thin models are always false
|
47
|
+
def persisted?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
# Associated model class
|
52
|
+
#
|
53
|
+
# @return [Class] of type Miasma::Types::Model
|
54
|
+
# @note will deconstruct namespace and rebuild using provider
|
55
|
+
def model
|
56
|
+
if(self.class.model)
|
57
|
+
self.class.model
|
58
|
+
else
|
59
|
+
raise NotImplementedError.new "No associated model for this thin model type (#{self.class})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Build fat model instance
|
64
|
+
#
|
65
|
+
# @return [Miasma::Types::Model]
|
66
|
+
def expand
|
67
|
+
inst = model.new(api)
|
68
|
+
inst.data[:id] = self.id
|
69
|
+
inst.reload
|
70
|
+
end
|
71
|
+
alias_method :instance, :expand
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
data/lib/miasma/utils.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Utils
|
5
|
+
autoload :Lazy, 'miasma/utils/lazy'
|
6
|
+
autoload :Memoization, 'miasma/utils/memoization'
|
7
|
+
autoload :Immutable, 'miasma/utils/immutable'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'miasma/utils/animal_strings'
|
12
|
+
require 'miasma/utils/smash'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
|
5
|
+
module Utils
|
6
|
+
# Animal stylings on strings
|
7
|
+
module AnimalStrings
|
8
|
+
|
9
|
+
# Camel case string
|
10
|
+
# @param string [String]
|
11
|
+
# @return [String]
|
12
|
+
def camel(string)
|
13
|
+
string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
|
14
|
+
end
|
15
|
+
|
16
|
+
# Snake case (underscore) string
|
17
|
+
#
|
18
|
+
# @param string [String]
|
19
|
+
# @return [String]
|
20
|
+
def snake(string)
|
21
|
+
string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').gsub('-', '_').downcase
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
extend AnimalStrings
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
|
5
|
+
module Utils
|
6
|
+
# Make best effort to make model immutable
|
7
|
+
# @note this should be included at end of model definition
|
8
|
+
module Immutable
|
9
|
+
|
10
|
+
# Freezes underlying data hash
|
11
|
+
def frozen_valid_state(*args)
|
12
|
+
unfrozen_valid_state(*args)
|
13
|
+
data.freeze
|
14
|
+
dirty.freeze
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# @raises [Error::ImmutableError]
|
19
|
+
def save
|
20
|
+
raise Error::ImmutableError.new 'Resource information cannot be mutated!'
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
|
25
|
+
def included(klass)
|
26
|
+
klass.class_eval do
|
27
|
+
alias_method :unfrozen_valid_state, :valid_state
|
28
|
+
alias_method :valid_state, :frozen_valid_state
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
require 'digest/sha2'
|
3
|
+
|
4
|
+
module Miasma
|
5
|
+
module Utils
|
6
|
+
# Adds functionality to facilitate laziness
|
7
|
+
module Lazy
|
8
|
+
|
9
|
+
# Instance methods for laziness
|
10
|
+
module InstanceMethods
|
11
|
+
|
12
|
+
# @return [Smash] argument hash
|
13
|
+
def data
|
14
|
+
unless(@data)
|
15
|
+
@data = Smash.new
|
16
|
+
end
|
17
|
+
@data
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Smash] updated data
|
21
|
+
def dirty
|
22
|
+
unless(@dirty)
|
23
|
+
@dirty = Smash.new
|
24
|
+
end
|
25
|
+
@dirty
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Smash] current data state
|
29
|
+
def attributes
|
30
|
+
data.merge(dirty)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create new instance
|
34
|
+
#
|
35
|
+
# @param args [Hash]
|
36
|
+
# @return [self]
|
37
|
+
def load_data(args={})
|
38
|
+
args = args.to_smash
|
39
|
+
@data = Smash.new
|
40
|
+
self.class.attributes.each do |name, options|
|
41
|
+
val = args[name]
|
42
|
+
if(options[:required] && !args.has_key?(name) && !options.has_key?(:default))
|
43
|
+
raise ArgumentError.new("Missing required option: `#{name}`")
|
44
|
+
end
|
45
|
+
if(val.nil? && !args.has_key?(name) && options[:default])
|
46
|
+
if(options[:default])
|
47
|
+
val = options[:default].respond_to?(:call) ? options[:default].call : options[:default]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
if(args.has_key?(name) || val)
|
51
|
+
self.send("#{name}=", val)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# Identifies valid state and automatically
|
58
|
+
# merges dirty attributes into data, clears
|
59
|
+
# dirty attributes
|
60
|
+
#
|
61
|
+
# @return [self]
|
62
|
+
def valid_state
|
63
|
+
data.merge!(dirty)
|
64
|
+
dirty.clear
|
65
|
+
@_checksum = Digest::SHA256.hexdigest(MultiJson.dump(data))
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Model is dirty or specific attribute is dirty
|
70
|
+
#
|
71
|
+
# @param attr [String, Symbol] name of attribute
|
72
|
+
# @return [TrueClass, FalseClass] model or attribute is dirty
|
73
|
+
def dirty?(attr=nil)
|
74
|
+
if(attr)
|
75
|
+
dirty.has_key?(attr)
|
76
|
+
else
|
77
|
+
if(@_checksum)
|
78
|
+
!dirty.empty? ||
|
79
|
+
@_checksum != Digest::SHA256.hexdigest(MultiJson.dump(data))
|
80
|
+
else
|
81
|
+
true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [String]
|
87
|
+
def to_s
|
88
|
+
"<#{self.class.name}:#{object_id}>"
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [String]
|
92
|
+
def inspect
|
93
|
+
"<#{self.class.name}:#{object_id} [#{data.inspect}]>"
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
# Class methods for laziness
|
99
|
+
module ClassMethods
|
100
|
+
|
101
|
+
# Add new attributes to class
|
102
|
+
#
|
103
|
+
# @param name [String]
|
104
|
+
# @param type [Class, Array<Class>]
|
105
|
+
# @param options [Hash]
|
106
|
+
# @option options [TrueClass, FalseClass] :required must be provided on initialization
|
107
|
+
# @option options [Object, Proc] :default default value
|
108
|
+
# @option options [Proc] :coerce
|
109
|
+
# @return [nil]
|
110
|
+
def attribute(name, type, options={})
|
111
|
+
name = name.to_sym
|
112
|
+
options = options.to_smash
|
113
|
+
attributes[name] = Smash.new(:type => type).merge(options)
|
114
|
+
coerce = attributes[name][:coerce]
|
115
|
+
valid_types = [attributes[name][:type], NilClass].flatten.compact
|
116
|
+
allowed_values = attributes[name][:allowed]
|
117
|
+
multiple_values = attributes[name][:multiple]
|
118
|
+
depends_on = attributes[name][:depends]
|
119
|
+
define_method(name) do
|
120
|
+
send(depends_on) if depends_on
|
121
|
+
self.class.on_missing(self) unless data.has_key?(name) || dirty.has_key?(name)
|
122
|
+
dirty[name] || data[name]
|
123
|
+
end
|
124
|
+
define_method("#{name}=") do |val|
|
125
|
+
values = multiple_values && val.is_a?(Array) ? val : [val]
|
126
|
+
values.map! do |item|
|
127
|
+
valid_type = valid_types.detect do |klass|
|
128
|
+
item.is_a?(klass)
|
129
|
+
end
|
130
|
+
if(coerce && !valid_type)
|
131
|
+
item = coerce.arity == 2 ? coerce.call(item, self) : coerce.call(item)
|
132
|
+
end
|
133
|
+
valid_type = valid_types.detect do |klass|
|
134
|
+
item.is_a?(klass)
|
135
|
+
end
|
136
|
+
unless(valid_type)
|
137
|
+
raise TypeError.new("Invalid type for `#{name}` (#{item} <#{item.class}>). Valid - #{valid_types.map(&:to_s).join(',')}")
|
138
|
+
end
|
139
|
+
if(allowed_values)
|
140
|
+
unless(allowed_values.include?(item))
|
141
|
+
raise ArgumentError.new("Invalid value provided for `#{name}` (#{item.inspect}). Allowed - #{allowed_values.map(&:inspect).join(', ')}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
item
|
145
|
+
end
|
146
|
+
if(!multiple_values && !val.is_a?(Array))
|
147
|
+
dirty[name] = values.first
|
148
|
+
else
|
149
|
+
dirty[name] = values
|
150
|
+
end
|
151
|
+
end
|
152
|
+
define_method("#{name}?") do
|
153
|
+
send(depends_on) if depends_on
|
154
|
+
self.class.on_missing(self) unless data.has_key?(name)
|
155
|
+
!!data[name]
|
156
|
+
end
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Return attributes
|
161
|
+
#
|
162
|
+
# @param args [Symbol] :required or :optional
|
163
|
+
# @return [Array<Hash>]
|
164
|
+
def attributes(*args)
|
165
|
+
@attributes ||= Smash.new
|
166
|
+
if(args.include?(:required))
|
167
|
+
Smash[@attributes.find_all{|k,v| v[:required]}]
|
168
|
+
elsif(args.include?(:optional))
|
169
|
+
Smash[@attributes.find_all{|k,v| !v[:required]}]
|
170
|
+
else
|
171
|
+
@attributes
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Instance method to call on missing attribute or
|
176
|
+
# object to call method on if set
|
177
|
+
#
|
178
|
+
# @param param [Symbol, Object]
|
179
|
+
# @return [Symbol]
|
180
|
+
def on_missing(param=nil)
|
181
|
+
if(param)
|
182
|
+
if(param.is_a?(Symbol))
|
183
|
+
@missing_method = param
|
184
|
+
else
|
185
|
+
if(@missing_method)
|
186
|
+
param.send(@missing_method)
|
187
|
+
end
|
188
|
+
@missing_method
|
189
|
+
end
|
190
|
+
else
|
191
|
+
@missing_method
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Directly set attribute hash
|
196
|
+
#
|
197
|
+
# @param attrs [Hash]
|
198
|
+
# @return [TrueClass]
|
199
|
+
# @todo need deep dup here
|
200
|
+
def set_attributes(attrs)
|
201
|
+
@attributes = attrs.to_smash
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
class << self
|
208
|
+
|
209
|
+
# Injects laziness into class
|
210
|
+
#
|
211
|
+
# @param klass [Class]
|
212
|
+
def included(klass)
|
213
|
+
klass.class_eval do
|
214
|
+
include InstanceMethods
|
215
|
+
extend ClassMethods
|
216
|
+
|
217
|
+
class << self
|
218
|
+
|
219
|
+
def inherited(klass)
|
220
|
+
klass.set_attributes(self.attributes)
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|