luna_park 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.overcommit.yml +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +106 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +308 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +182 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/Rakefile +30 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +190 -0
- data/docs/_coverpage.md +18 -0
- data/docs/_imgs/adapter.png +0 -0
- data/docs/_imgs/bender.jpeg +0 -0
- data/docs/_imgs/bender_header.jpeg +0 -0
- data/docs/_imgs/collecting.png +0 -0
- data/docs/_imgs/conductor_schema.png +0 -0
- data/docs/_imgs/ddd_header.jpeg +0 -0
- data/docs/_imgs/ddd_map.png +0 -0
- data/docs/_imgs/domain_context.jpeg +0 -0
- data/docs/_imgs/drunk_master.jpg +0 -0
- data/docs/_imgs/full_map.png +0 -0
- data/docs/_imgs/full_map_hight_res.png +0 -0
- data/docs/_imgs/graph_1.png +0 -0
- data/docs/_imgs/graph_2.jpg +0 -0
- data/docs/_imgs/graph_3.png +0 -0
- data/docs/_imgs/graph_4.jpg +0 -0
- data/docs/_imgs/graph_5.jpg +0 -0
- data/docs/_imgs/graph_5.png +0 -0
- data/docs/_imgs/processing.png +0 -0
- data/docs/_imgs/representation.png +0 -0
- data/docs/_imgs/storage.png +0 -0
- data/docs/_imgs/tourtle_context_map.png +0 -0
- data/docs/_imgs/tree.png +0 -0
- data/docs/_imgs/wm.jpeg +0 -0
- data/docs/_media/bender.jpg +0 -0
- data/docs/_media/black_cover.jpg +0 -0
- data/docs/_media/logo.svg +7 -0
- data/docs/_sidebar.md +9 -0
- data/docs/architecture.md +214 -0
- data/docs/google48f1e6f5c35eae5f.html +1 -0
- data/docs/index.html +32 -0
- data/docs/methodology.md +376 -0
- data/docs/patterns/entity.md +193 -0
- data/docs/patterns/sequence.md +332 -0
- data/docs/patterns/service.md +280 -0
- data/docs/patterns/value.md +197 -0
- data/docs/way.md +66 -0
- data/lib/luna_park.rb +72 -0
- data/lib/luna_park/callable.rb +7 -0
- data/lib/luna_park/entities/attributable.rb +18 -0
- data/lib/luna_park/entities/nested.rb +28 -0
- data/lib/luna_park/entities/simple.rb +39 -0
- data/lib/luna_park/errors.rb +16 -0
- data/lib/luna_park/errors/base.rb +244 -0
- data/lib/luna_park/errors/business.rb +9 -0
- data/lib/luna_park/errors/http.rb +90 -0
- data/lib/luna_park/errors/json_parse.rb +11 -0
- data/lib/luna_park/errors/system.rb +9 -0
- data/lib/luna_park/extensions/attributable.rb +26 -0
- data/lib/luna_park/extensions/callable.rb +44 -0
- data/lib/luna_park/extensions/comparable.rb +90 -0
- data/lib/luna_park/extensions/comparable_debug.rb +96 -0
- data/lib/luna_park/extensions/data_mapper.rb +195 -0
- data/lib/luna_park/extensions/dsl/attributes.rb +135 -0
- data/lib/luna_park/extensions/dsl/foreign_key.rb +97 -0
- data/lib/luna_park/extensions/exceptions/substitutive.rb +83 -0
- data/lib/luna_park/extensions/has_errors.rb +125 -0
- data/lib/luna_park/extensions/injector.rb +189 -0
- data/lib/luna_park/extensions/injector/dependencies.rb +74 -0
- data/lib/luna_park/extensions/predicate_attr_accessor.rb +23 -0
- data/lib/luna_park/extensions/repositories/postgres/create.rb +20 -0
- data/lib/luna_park/extensions/repositories/postgres/delete.rb +15 -0
- data/lib/luna_park/extensions/repositories/postgres/read.rb +63 -0
- data/lib/luna_park/extensions/repositories/postgres/update.rb +21 -0
- data/lib/luna_park/extensions/serializable.rb +99 -0
- data/lib/luna_park/extensions/severity_levels.rb +120 -0
- data/lib/luna_park/extensions/typed_attr_accessor.rb +26 -0
- data/lib/luna_park/extensions/validatable.rb +80 -0
- data/lib/luna_park/extensions/validatable/dry.rb +24 -0
- data/lib/luna_park/extensions/wrappable.rb +43 -0
- data/lib/luna_park/forms/simple.rb +63 -0
- data/lib/luna_park/forms/single_item.rb +74 -0
- data/lib/luna_park/handlers/simple.rb +17 -0
- data/lib/luna_park/http/client.rb +328 -0
- data/lib/luna_park/http/request.rb +225 -0
- data/lib/luna_park/http/response.rb +381 -0
- data/lib/luna_park/http/send.rb +103 -0
- data/lib/luna_park/mappers/simple.rb +92 -0
- data/lib/luna_park/notifiers/bugsnag.rb +48 -0
- data/lib/luna_park/notifiers/log.rb +174 -0
- data/lib/luna_park/notifiers/sentry.rb +50 -0
- data/lib/luna_park/repositories/postgres.rb +38 -0
- data/lib/luna_park/repositories/sequel.rb +11 -0
- data/lib/luna_park/repository.rb +9 -0
- data/lib/luna_park/serializers/simple.rb +28 -0
- data/lib/luna_park/tools.rb +19 -0
- data/lib/luna_park/use_cases/scenario.rb +325 -0
- data/lib/luna_park/use_cases/service.rb +13 -0
- data/lib/luna_park/validators/dry.rb +67 -0
- data/lib/luna_park/values/attributable.rb +21 -0
- data/lib/luna_park/values/compound.rb +26 -0
- data/lib/luna_park/values/single.rb +35 -0
- data/lib/luna_park/version.rb +5 -0
- data/luna_park.gemspec +54 -0
- data/node_modules/.yarn-integrity +12 -0
- data/package-lock.json +3 -0
- data/yarn.lock +4 -0
- metadata +414 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module Injector
|
6
|
+
##
|
7
|
+
# Hash for define dependencies in {Injector} extension.
|
8
|
+
#
|
9
|
+
# Main difference between Hash and Dependencies it is memorization;
|
10
|
+
#
|
11
|
+
# # hash example
|
12
|
+
# i = 0
|
13
|
+
#
|
14
|
+
# hash = { i: -> { i += 1 } }
|
15
|
+
# hash[:i].call # => 1
|
16
|
+
# hash[:i].call # => 2
|
17
|
+
#
|
18
|
+
# # dependencies
|
19
|
+
# i = 0
|
20
|
+
#
|
21
|
+
# dependencies = Dependencies.wrap(i: -> { i += 1 })
|
22
|
+
# dependencies.call_with_cache(:i) # => 1
|
23
|
+
# dependencies.call_with_cache(:i) # => 1
|
24
|
+
#
|
25
|
+
class Dependencies < Hash
|
26
|
+
class << self
|
27
|
+
##
|
28
|
+
# Dependencies.try_convert(obj) -> hash or nil
|
29
|
+
#
|
30
|
+
# Try to convert obj into a hash, using to_hash method.
|
31
|
+
# Returns converted hash or nil if obj cannot be converted
|
32
|
+
# for any reason.
|
33
|
+
#
|
34
|
+
# See {Hash.try_convert}[https://ruby-doc.org/core-2.7.2/Hash.html#method-c-try_convert]
|
35
|
+
#
|
36
|
+
# Dependencies.try_convert({1=>2}) # => {1=>2}
|
37
|
+
# Dependencies.try_convert("1=>2") # => nil
|
38
|
+
def try_convert(obj)
|
39
|
+
super.nil? ? nil : new.replace(super)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias wrap try_convert
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Run dependency code and cache result.
|
47
|
+
#
|
48
|
+
# use_case.dependencies[:messenger] # => #<Proc:0x0000564a0d90d438@t.rb:34>
|
49
|
+
# use_case.dependencies.call_with_cache(:messenger) # => 'Foobar'
|
50
|
+
def call_with_cache(key)
|
51
|
+
cache[key] ||= self[key].call
|
52
|
+
end
|
53
|
+
|
54
|
+
def []=(key, _val)
|
55
|
+
cache[key] = nil
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def cache
|
62
|
+
@cache ||= {}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @!parse include Injector::ClassMethods
|
67
|
+
# @!parse extend Injector::InstanceMethods
|
68
|
+
def self.included(base)
|
69
|
+
base.extend ClassMethods
|
70
|
+
base.include InstanceMethods
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module PredicateAttrAccessor
|
6
|
+
def predicate_attr_accessor(*names)
|
7
|
+
attr_writer(*names)
|
8
|
+
attr_reader?(*names)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias attr_accessor? predicate_attr_accessor
|
12
|
+
|
13
|
+
def predicate_attr_reader(*names)
|
14
|
+
names.each do |name|
|
15
|
+
ivar = :"@#{name}"
|
16
|
+
define_method(:"#{name}?") { instance_variable_get(ivar) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
alias attr_reader? predicate_attr_reader
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module Repositories
|
6
|
+
module Postgres
|
7
|
+
module Create
|
8
|
+
def create(input)
|
9
|
+
entity = wrap(input)
|
10
|
+
row = to_row(entity)
|
11
|
+
new_row = dataset.returning.insert(row).first
|
12
|
+
new_attrs = from_row(new_row)
|
13
|
+
entity.set_attributes(new_attrs)
|
14
|
+
entity
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module Repositories
|
6
|
+
module Postgres
|
7
|
+
module Read
|
8
|
+
def find!(pk_value, for_update: false)
|
9
|
+
ds = dataset.where(primary_key => pk_value)
|
10
|
+
read_one!(ds, for_update: for_update, not_found_meta: pk_value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(pk_value, for_update: false)
|
14
|
+
ds = dataset.where(primary_key => pk_value)
|
15
|
+
read_one(ds, for_update: for_update)
|
16
|
+
end
|
17
|
+
|
18
|
+
def lock!(pk_value)
|
19
|
+
lock(pk_value) || raise(Errors::NotFound, "#{short_class_name} (#{pk_value})")
|
20
|
+
end
|
21
|
+
|
22
|
+
def lock(pk_value)
|
23
|
+
dataset.for_update.select(primary_key).where(primary_key => pk_value).first ? true : false
|
24
|
+
end
|
25
|
+
|
26
|
+
def count
|
27
|
+
dataset.count
|
28
|
+
end
|
29
|
+
|
30
|
+
def all
|
31
|
+
read_all(dataset.order { created_at.desc })
|
32
|
+
end
|
33
|
+
|
34
|
+
def last
|
35
|
+
to_entity from_row dataset.order(:created_at).last
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def read_one!(dataset, for_update: false, not_found_meta:)
|
41
|
+
read_one(dataset, for_update: for_update).tap do |entity|
|
42
|
+
raise Errors::NotFound, "#{short_class_name} (#{not_found_meta})" if entity.nil?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_one(dataset, for_update: false)
|
47
|
+
dataset = dataset.for_update if for_update
|
48
|
+
row = dataset.first
|
49
|
+
to_entity from_row(row)
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_all(dataset)
|
53
|
+
to_entities from_rows(dataset)
|
54
|
+
end
|
55
|
+
|
56
|
+
def short_class_name
|
57
|
+
@short_class_name ||= self.class.name[/::(\w+)\z/, 1]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module Repositories
|
6
|
+
module Postgres
|
7
|
+
module Update
|
8
|
+
def save(input)
|
9
|
+
entity = wrap(input)
|
10
|
+
entity.updated_at = Time.now.utc
|
11
|
+
row = to_row(entity)
|
12
|
+
new_row = dataset.returning.where(primary_key => row[primary_key]).update(row).first
|
13
|
+
new_attrs = from_row(new_row)
|
14
|
+
entity.set_attributes(new_attrs)
|
15
|
+
entity
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'luna_park/errors'
|
4
|
+
|
5
|
+
module LunaPark
|
6
|
+
module Extensions
|
7
|
+
# @example
|
8
|
+
# class Money
|
9
|
+
# include LunaPark::Extensions::Comparable
|
10
|
+
#
|
11
|
+
# attr_accessor :amount, :currency, :meta
|
12
|
+
#
|
13
|
+
# serializable_attributes :amount, :currency
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# money = Money.new
|
17
|
+
# money.to_h # => {}
|
18
|
+
# money.amount = 1
|
19
|
+
# money.to_h # => { amount: 1 }
|
20
|
+
# money.currency = 'USD'
|
21
|
+
# money.meta = 'meta'
|
22
|
+
# money.to_h # => { amount: 1, currency: 'USD' }
|
23
|
+
module Serializable
|
24
|
+
def self.included(base)
|
25
|
+
base.extend ClassMethods
|
26
|
+
base.include InstanceMethods
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
##
|
31
|
+
# Describe methods list that will be used for serialization via `#to_h` and `#serialize` methods
|
32
|
+
def serializable_attributes(*names)
|
33
|
+
raise 'No attributes given' if names.compact.empty?
|
34
|
+
|
35
|
+
@serializable_attributes_list ||= []
|
36
|
+
@serializable_attributes_list |= names
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# List of methods that will be used for serialization via `#to_h` and `#serialize` methods
|
41
|
+
def serializable_attributes_list
|
42
|
+
return @serializable_attributes_list if @serializable_attributes_list
|
43
|
+
|
44
|
+
raise Errors::NotConfigured,
|
45
|
+
"You must set at least one serializable attribute using #{self}.serializable_attributes(*names)"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def inherited(child)
|
51
|
+
super
|
52
|
+
child.instance_variable_set(:@serializable_attributes_list, @serializable_attributes_list&.dup)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module InstanceMethods
|
57
|
+
##
|
58
|
+
# Serialize object using methods, described with `::comparable_attributes` method
|
59
|
+
def serialize
|
60
|
+
self.class
|
61
|
+
.serializable_attributes_list
|
62
|
+
.each_with_object({}) do |field, output|
|
63
|
+
next unless instance_variable_defined?(:"@#{field}")
|
64
|
+
|
65
|
+
output[field] = serialize_value__(send(field))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# For powerfull polymorphism with Hashes
|
71
|
+
alias to_h serialize
|
72
|
+
|
73
|
+
def inspect
|
74
|
+
attrs = self.class.serializable_attributes_list.map do |attr|
|
75
|
+
value = instance_variable_get(:"@#{attr}")
|
76
|
+
"#{attr}=#{value.inspect}" if value
|
77
|
+
end
|
78
|
+
"#<#{self.class.name} #{attrs.compact.join(' ')}>"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
SERIALIZABLE = ->(o) { o.respond_to?(:serialize) }.freeze
|
84
|
+
HASHABLE = ->(o) { o.respond_to?(:to_h) }.freeze
|
85
|
+
|
86
|
+
def serialize_value__(value) # rubocop:disable Metrics/CyclomaticComplexity
|
87
|
+
case value
|
88
|
+
when Array then value.map { |v| serialize_value__(v) } # TODO: work with Array (wrap values)
|
89
|
+
when Hash then value.transform_values { |v| serialize_value__(v) }
|
90
|
+
when nil then nil
|
91
|
+
when SERIALIZABLE then value.serialize
|
92
|
+
when HASHABLE then value.to_h
|
93
|
+
else value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LunaPark
|
4
|
+
module Extensions
|
5
|
+
module SeverityLevels
|
6
|
+
# This is class define interface for loggers and notifiers behavior.
|
7
|
+
# In main idea it based on rfc5424 https://tools.ietf.org/html/rfc5424, but
|
8
|
+
# in fact default ruby logger does not define all severities, and we use only
|
9
|
+
# most important:
|
10
|
+
# - unknown: an unknown message that should always be logged
|
11
|
+
# - fatal: An unhandleable error that results in a program crash
|
12
|
+
# - error: system work incorrectly, and maintainer should know about that immediately
|
13
|
+
# - warning: warning conditions, and maintainer should know about that, but not immediately
|
14
|
+
# - info: informational messages, maintainer should know about that, if they want to analyse logs
|
15
|
+
# - debug: debug messages, for developers don't use it on production
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# class ChattyLogger < LunaPark::Notifiers::Abstract
|
19
|
+
# def message(obj, _details:, lvl:)
|
20
|
+
# puts "{lvl.upcase}: #{message}"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# logger = ChattyLogger.new min_lvl: :warning
|
25
|
+
# logger.unknown 'Do not do that.' # => 'UNKNOWN: Do not do that.'
|
26
|
+
# logger.fatal 'Do not do that.' # => 'FATAL: Do not do that.'
|
27
|
+
# logger.error 'Do not do that.' # => 'ERROR: Do not do that.'
|
28
|
+
# logger.warning 'Do not do that.' # => 'WARNING: Do not do that.'
|
29
|
+
# logger.info 'Do not do that.' # => nil
|
30
|
+
# logger.debug 'Do not do that.' # => nil
|
31
|
+
LEVELS = %i[debug info warning error fatal unknown].freeze
|
32
|
+
|
33
|
+
# Defined minimum severity level
|
34
|
+
def min_lvl
|
35
|
+
@min_lvl ||= :debug
|
36
|
+
end
|
37
|
+
|
38
|
+
def min_lvl=(value)
|
39
|
+
raise ArgumentError, 'Undefined severity level' unless LEVELS.include? value
|
40
|
+
|
41
|
+
@min_lvl = value
|
42
|
+
end
|
43
|
+
|
44
|
+
# rubocop:disable Style/GuardClause
|
45
|
+
|
46
|
+
# Post message with UNKNOWN severity level
|
47
|
+
#
|
48
|
+
# @param msg [String,Exception]
|
49
|
+
# @param details [Hash]
|
50
|
+
def unknown(msg = '', **details)
|
51
|
+
message = block_given? ? yield : msg
|
52
|
+
post message, lvl: :unknown, **details
|
53
|
+
end
|
54
|
+
|
55
|
+
# Post message with FATAL severity level
|
56
|
+
#
|
57
|
+
# @param msg [String,Exception]
|
58
|
+
# @param details [Hash]
|
59
|
+
def fatal(msg = '', **details)
|
60
|
+
if %i[debug info warning error fatal].include? min_lvl
|
61
|
+
message = block_given? ? yield : msg
|
62
|
+
post message, lvl: :fatal, **details
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Post message with ERROR severity level
|
67
|
+
#
|
68
|
+
# @param msg [String,Exception]
|
69
|
+
# @param details [Hash]
|
70
|
+
def error(msg = '', **details)
|
71
|
+
if %i[debug info warning error].include? min_lvl
|
72
|
+
message = block_given? ? yield : msg
|
73
|
+
post message, lvl: :error, **details
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Post stdout message with WARNING severity level
|
78
|
+
#
|
79
|
+
# @param msg [String,Exception]
|
80
|
+
# @param details [Hash]
|
81
|
+
def warning(msg = '', **details)
|
82
|
+
if %i[debug info warning].include? min_lvl
|
83
|
+
message = block_given? ? yield : msg
|
84
|
+
post message, lvl: :warning, **details
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Post message with INFO severity level
|
89
|
+
#
|
90
|
+
# @example
|
91
|
+
#
|
92
|
+
# @param msg [String,Exception]
|
93
|
+
# @param details [Hash]
|
94
|
+
def info(msg = '', **details)
|
95
|
+
if %i[debug info].include? min_lvl
|
96
|
+
message = block_given? ? yield : msg
|
97
|
+
post message, lvl: :info, **details
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Post message with DEBUG severity level
|
102
|
+
#
|
103
|
+
# @param msg [String,Exception]
|
104
|
+
# @param details [Hash]
|
105
|
+
def debug(msg = '', **details)
|
106
|
+
if min_lvl == :debug
|
107
|
+
message = block_given? ? yield : msg
|
108
|
+
post message, lvl: :debug, **details
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# rubocop:enable Style/GuardClause
|
113
|
+
|
114
|
+
# @abstract
|
115
|
+
def post(_msg = '', _lvl:, **_details)
|
116
|
+
raise Errors::AbstractMethod
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|