miasma 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|