active_remote 5.1.1 → 5.2.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/active_remote.gemspec +2 -2
- data/bin/benchmark +12 -5
- data/lib/active_remote/association.rb +7 -9
- data/lib/active_remote/attribute_methods.rb +51 -0
- data/lib/active_remote/base.rb +64 -14
- data/lib/active_remote/dirty.rb +4 -7
- data/lib/active_remote/integration.rb +112 -26
- data/lib/active_remote/persistence.rb +1 -1
- data/lib/active_remote/primary_key.rb +18 -0
- data/lib/active_remote/rpc.rb +3 -4
- data/lib/active_remote/search.rb +2 -1
- data/lib/active_remote/version.rb +1 -1
- data/spec/lib/active_remote/association_spec.rb +0 -16
- data/spec/lib/active_remote/dirty_spec.rb +14 -29
- data/spec/lib/active_remote/integration_spec.rb +80 -12
- data/spec/lib/active_remote/query_attribute_spec.rb +8 -17
- data/spec/lib/active_remote/rpc_spec.rb +5 -1
- data/spec/lib/active_remote/search_spec.rb +4 -3
- data/spec/support/models.rb +0 -1
- data/spec/support/models/author.rb +10 -6
- data/spec/support/models/category.rb +3 -3
- data/spec/support/models/default_author.rb +2 -2
- data/spec/support/models/post.rb +5 -5
- data/spec/support/models/tag.rb +4 -4
- metadata +10 -16
- data/lib/active_remote/attribute_definition.rb +0 -114
- data/lib/active_remote/attributes.rb +0 -204
- data/spec/lib/active_remote/attributes_spec.rb +0 -223
- data/spec/support/models/typecasted_author.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d8212d6102d24ca632d2f76449826bf4593b2a76a0d27269866a0d3533b4813
|
4
|
+
data.tar.gz: e648845212e9756304427bccfc60da60d2aaf2325e309639905cfeb163e234bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89eed8d66c19ddc8daecd21db103ff91d29a5d2b779770163e7962a85a33c32c4c2d41f64ae8e27d950e87ddfa563350058a3478aa56e4b6ce50618405b1582f
|
7
|
+
data.tar.gz: d137fab5f02b7f753be9122f6aabe18c9593456a740384e1da0b80aca8ac1d9eaaaf6e8c8ca5c7d5cd2dae58c971ac04e0f5c274301d90a71f3ae1f3215502f9
|
data/active_remote.gemspec
CHANGED
@@ -20,8 +20,8 @@ Gem::Specification.new do |s|
|
|
20
20
|
##
|
21
21
|
# Dependencies
|
22
22
|
#
|
23
|
-
s.add_dependency "activemodel", "~> 5.
|
24
|
-
s.add_dependency "activesupport", "~> 5.
|
23
|
+
s.add_dependency "activemodel", "~> 5.2"
|
24
|
+
s.add_dependency "activesupport", "~> 5.2"
|
25
25
|
s.add_dependency "protobuf", ">= 3.0"
|
26
26
|
|
27
27
|
##
|
data/bin/benchmark
CHANGED
@@ -5,7 +5,14 @@ require "bundler/setup"
|
|
5
5
|
require "active_remote"
|
6
6
|
require "benchmark/ips"
|
7
7
|
|
8
|
-
|
8
|
+
class Author < ::ActiveRemote::Base
|
9
|
+
attribute :guid, :string
|
10
|
+
attribute :name, :string
|
11
|
+
attribute :age, :integer
|
12
|
+
attribute :birthday, :datetime
|
13
|
+
attribute :writes_fiction, :boolean
|
14
|
+
attribute :net_sales, :float
|
15
|
+
end
|
9
16
|
|
10
17
|
ATTRIBUTES = {
|
11
18
|
:guid => "0c030733-3b78-4587-b94b-5e0cf26497c5",
|
@@ -19,18 +26,18 @@ ATTRIBUTES = {
|
|
19
26
|
::Benchmark.ips do |x|
|
20
27
|
x.config(:time => 20, :warmup => 10)
|
21
28
|
x.report("initialize") do
|
22
|
-
::
|
29
|
+
::Author.new(ATTRIBUTES)
|
23
30
|
end
|
24
31
|
|
25
32
|
x.report("instantiate") do
|
26
|
-
::
|
33
|
+
::Author.instantiate(ATTRIBUTES)
|
27
34
|
end
|
28
35
|
|
29
36
|
x.report("init attributes") do
|
30
|
-
::
|
37
|
+
::Author.new(ATTRIBUTES).attributes
|
31
38
|
end
|
32
39
|
|
33
40
|
x.report("inst attributes") do
|
34
|
-
::
|
41
|
+
::Author.instantiate(ATTRIBUTES).attributes
|
35
42
|
end
|
36
43
|
end
|
@@ -8,8 +8,7 @@ module ActiveRemote
|
|
8
8
|
# class must be loaded into memory already. A method will be defined
|
9
9
|
# with the same name as the association. When invoked, the associated
|
10
10
|
# remote model will issue a `search` for the :guid with the associated
|
11
|
-
# guid
|
12
|
-
# remote object from the result, or nil.
|
11
|
+
# guid attribute and return the first remote object from the result, or nil.
|
13
12
|
#
|
14
13
|
# A `belongs_to` association should be used when the associating remote
|
15
14
|
# contains the guid to the associated model. For example, if a User model
|
@@ -37,8 +36,8 @@ module ActiveRemote
|
|
37
36
|
perform_association(belongs_to_klass, options) do |klass, object|
|
38
37
|
foreign_key = options.fetch(:foreign_key) { :"#{klass.name.demodulize.underscore}_guid" }
|
39
38
|
search_hash = {}
|
40
|
-
search_hash[:guid] = object.
|
41
|
-
search_hash[options[:scope]] = object.
|
39
|
+
search_hash[:guid] = object.send(foreign_key)
|
40
|
+
search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
|
42
41
|
|
43
42
|
search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
|
44
43
|
end
|
@@ -49,7 +48,7 @@ module ActiveRemote
|
|
49
48
|
# class must be loaded into memory already. A method will be defined
|
50
49
|
# with the same plural name as the association. When invoked, the associated
|
51
50
|
# remote model will issue a `search` for the :guid with the associated
|
52
|
-
# guid
|
51
|
+
# guid attribute.
|
53
52
|
#
|
54
53
|
# A `has_many` association should be used when the associated model has
|
55
54
|
# a field to identify the associating model, and there can be multiple
|
@@ -79,7 +78,7 @@ module ActiveRemote
|
|
79
78
|
foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
|
80
79
|
search_hash = {}
|
81
80
|
search_hash[foreign_key] = object.guid
|
82
|
-
search_hash[options[:scope]] = object.
|
81
|
+
search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
|
83
82
|
|
84
83
|
search_hash.values.any?(&:nil?) ? [] : klass.search(search_hash)
|
85
84
|
end
|
@@ -93,8 +92,7 @@ module ActiveRemote
|
|
93
92
|
# class must be loaded into memory already. A method will be defined
|
94
93
|
# with the same name as the association. When invoked, the associated
|
95
94
|
# remote model will issue a `search` for the :guid with the associated
|
96
|
-
# guid
|
97
|
-
# remote object in the result, or nil.
|
95
|
+
# guid attribute and return the first remote object in the result, or nil.
|
98
96
|
#
|
99
97
|
# A `has_one` association should be used when the associated remote
|
100
98
|
# contains the guid from the associating model. For example, if a User model
|
@@ -120,7 +118,7 @@ module ActiveRemote
|
|
120
118
|
foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
|
121
119
|
search_hash = {}
|
122
120
|
search_hash[foreign_key] = object.guid
|
123
|
-
search_hash[options[:scope]] = object.
|
121
|
+
search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
|
124
122
|
|
125
123
|
search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
|
126
124
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ActiveRemote
|
2
|
+
module AttributeMethods
|
3
|
+
extend ::ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def attribute_names
|
7
|
+
@attribute_names ||= attribute_types.keys
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
attribute(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(name, value)
|
16
|
+
write_attribute(name, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an <tt>#inspect</tt>-like string for the value of the
|
20
|
+
# attribute +attr_name+. String attributes are truncated up to 50
|
21
|
+
# characters, Date and Time attributes are returned in the
|
22
|
+
# <tt>:db</tt> format. Other attributes return the value of
|
23
|
+
# <tt>#inspect</tt> without modification.
|
24
|
+
#
|
25
|
+
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
|
26
|
+
#
|
27
|
+
# person.attribute_for_inspect(:name)
|
28
|
+
# # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
|
29
|
+
#
|
30
|
+
# person.attribute_for_inspect(:created_at)
|
31
|
+
# # => "\"2012-10-22 00:15:07\""
|
32
|
+
#
|
33
|
+
# person.attribute_for_inspect(:tag_ids)
|
34
|
+
# # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
|
35
|
+
def attribute_for_inspect(attr_name)
|
36
|
+
value = attribute(attr_name)
|
37
|
+
|
38
|
+
if value.is_a?(String) && value.length > 50
|
39
|
+
"#{value[0, 50]}...".inspect
|
40
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
41
|
+
%("#{value.to_s(:db)}")
|
42
|
+
else
|
43
|
+
value.inspect
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def attribute_names
|
48
|
+
@attributes.keys
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/active_remote/base.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
require "active_model/callbacks"
|
2
2
|
|
3
3
|
require "active_remote/association"
|
4
|
-
require "active_remote/
|
5
|
-
require "active_remote/attributes"
|
4
|
+
require "active_remote/attribute_methods"
|
6
5
|
require "active_remote/config"
|
7
6
|
require "active_remote/dirty"
|
8
7
|
require "active_remote/dsl"
|
@@ -21,14 +20,16 @@ module ActiveRemote
|
|
21
20
|
extend ::ActiveModel::Callbacks
|
22
21
|
|
23
22
|
include ::ActiveModel::Model
|
23
|
+
include ::ActiveModel::Attributes
|
24
24
|
|
25
25
|
include ::ActiveRemote::Association
|
26
|
-
include ::ActiveRemote::
|
26
|
+
include ::ActiveRemote::AttributeMethods
|
27
27
|
include ::ActiveRemote::DSL
|
28
28
|
include ::ActiveRemote::Integration
|
29
|
+
include ::ActiveRemote::QueryAttributes
|
29
30
|
include ::ActiveRemote::Persistence
|
30
31
|
include ::ActiveRemote::PrimaryKey
|
31
|
-
|
32
|
+
|
32
33
|
include ::ActiveRemote::RPC
|
33
34
|
include ::ActiveRemote::ScopeKeys
|
34
35
|
include ::ActiveRemote::Search
|
@@ -45,10 +46,7 @@ module ActiveRemote
|
|
45
46
|
define_model_callbacks :initialize, :only => :after
|
46
47
|
|
47
48
|
def initialize(attributes = {})
|
48
|
-
|
49
|
-
|
50
|
-
assign_attributes(attributes) if attributes
|
51
|
-
|
49
|
+
super
|
52
50
|
@new_record = true
|
53
51
|
|
54
52
|
skip_dirty_tracking do
|
@@ -58,6 +56,41 @@ module ActiveRemote
|
|
58
56
|
end
|
59
57
|
end
|
60
58
|
|
59
|
+
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
|
60
|
+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
|
61
|
+
#
|
62
|
+
# Note that new records are different from any other record by definition, unless the
|
63
|
+
# other record is the receiver itself. Besides, if you fetch existing records with
|
64
|
+
# +select+ and leave the ID out, you're on your own, this predicate will return false.
|
65
|
+
#
|
66
|
+
# Note also that destroying a record preserves its ID in the model instance, so deleted
|
67
|
+
# models are still comparable.
|
68
|
+
def ==(other)
|
69
|
+
super ||
|
70
|
+
other.instance_of?(self.class) &&
|
71
|
+
!send(primary_key).nil? &&
|
72
|
+
other.send(primary_key) == send(primary_key)
|
73
|
+
end
|
74
|
+
alias_method :eql?, :==
|
75
|
+
|
76
|
+
# Allows sort on objects
|
77
|
+
def <=>(other)
|
78
|
+
if other.is_a?(self.class)
|
79
|
+
to_key <=> other.to_key
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def freeze
|
86
|
+
@attributes.freeze
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def frozen?
|
91
|
+
@attributes.frozen?
|
92
|
+
end
|
93
|
+
|
61
94
|
# Initialize an object with the attributes hash directly
|
62
95
|
# When used with allocate, bypasses initialize
|
63
96
|
def init_with(attributes)
|
@@ -69,15 +102,32 @@ module ActiveRemote
|
|
69
102
|
self
|
70
103
|
end
|
71
104
|
|
72
|
-
|
73
|
-
|
74
|
-
|
105
|
+
# Returns the contents of the record as a nicely formatted string.
|
106
|
+
def inspect
|
107
|
+
# We check defined?(@attributes) not to issue warnings if the object is
|
108
|
+
# allocated but not initialized.
|
109
|
+
inspection = if defined?(@attributes) && @attributes
|
110
|
+
attribute_names.collect do |name, _|
|
111
|
+
if attribute?(name)
|
112
|
+
"#{name}: #{attribute_for_inspect(name)}"
|
113
|
+
else
|
114
|
+
name
|
115
|
+
end
|
116
|
+
end.compact.join(", ")
|
117
|
+
else
|
118
|
+
"not initialized"
|
119
|
+
end
|
120
|
+
|
121
|
+
"#<#{self.class} #{inspection}>"
|
75
122
|
end
|
76
123
|
|
77
|
-
|
78
|
-
|
124
|
+
# Returns a hash of the given methods with their names as keys and returned values as values.
|
125
|
+
def slice(*methods)
|
126
|
+
Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
|
79
127
|
end
|
80
128
|
end
|
81
129
|
|
82
|
-
|
130
|
+
::ActiveModel::Type.register(:value, ::ActiveModel::Type::Value)
|
131
|
+
|
132
|
+
::ActiveSupport.run_load_hooks(:active_remote, Base)
|
83
133
|
end
|
data/lib/active_remote/dirty.rb
CHANGED
@@ -24,8 +24,7 @@ module ActiveRemote
|
|
24
24
|
#
|
25
25
|
def reload(*)
|
26
26
|
super.tap do
|
27
|
-
|
28
|
-
changed_attributes.clear
|
27
|
+
clear_changes_information
|
29
28
|
end
|
30
29
|
end
|
31
30
|
|
@@ -41,8 +40,7 @@ module ActiveRemote
|
|
41
40
|
#
|
42
41
|
def save(*)
|
43
42
|
if (status = super)
|
44
|
-
|
45
|
-
changed_attributes.clear
|
43
|
+
changes_applied
|
46
44
|
end
|
47
45
|
|
48
46
|
status
|
@@ -52,8 +50,7 @@ module ActiveRemote
|
|
52
50
|
#
|
53
51
|
def save!(*)
|
54
52
|
super.tap do
|
55
|
-
|
56
|
-
changed_attributes.clear
|
53
|
+
changes_applied
|
57
54
|
end
|
58
55
|
end
|
59
56
|
|
@@ -77,7 +74,7 @@ module ActiveRemote
|
|
77
74
|
# ActiveModel::Dirty.
|
78
75
|
#
|
79
76
|
def attribute=(name, value)
|
80
|
-
|
77
|
+
send("#{name}_will_change!") if _active_remote_track_changes? && value != self[name]
|
81
78
|
super
|
82
79
|
end
|
83
80
|
|
@@ -3,48 +3,134 @@ module ActiveRemote
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
##
|
7
|
+
# :singleton-method:
|
8
|
+
# Indicates the format used to generate the timestamp in the cache key, if
|
9
|
+
# versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
|
10
|
+
#
|
11
|
+
# This is +:usec+, by default.
|
12
|
+
class_attribute :cache_timestamp_format, :instance_writer => false, :default => :usec
|
13
|
+
|
14
|
+
##
|
15
|
+
# :singleton-method:
|
16
|
+
# Indicates whether to use a stable #cache_key method that is accompanied
|
17
|
+
# by a changing version in the #cache_version method.
|
18
|
+
#
|
19
|
+
# This is +false+, by default until Rails 6.0.
|
20
|
+
class_attribute :cache_versioning, :instance_writer => false, :default => false
|
16
21
|
end
|
17
22
|
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
23
|
+
# Returns a +String+, which Action Pack uses for constructing a URL to this
|
24
|
+
# object. The default implementation returns this record's id as a +String+,
|
25
|
+
# or +nil+ if this record's unsaved.
|
26
|
+
#
|
27
|
+
# For example, suppose that you have a User model, and that you have a
|
28
|
+
# <tt>resources :users</tt> route. Normally, +user_path+ will
|
29
|
+
# construct a path with the user object's 'id' in it:
|
30
|
+
#
|
31
|
+
# user = User.find_by(name: 'Phusion')
|
32
|
+
# user_path(user) # => "/users/1"
|
33
|
+
#
|
34
|
+
# You can override +to_param+ in your model to make +user_path+ construct
|
35
|
+
# a path using the user's name instead of the user's id:
|
22
36
|
#
|
23
|
-
#
|
24
|
-
#
|
37
|
+
# class User < ActiveRecord::Base
|
38
|
+
# def to_param # overridden
|
39
|
+
# name
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# user = User.find_by(name: 'Phusion')
|
44
|
+
# user_path(user) # => "/users/Phusion"
|
25
45
|
#
|
26
46
|
def to_param
|
27
|
-
|
47
|
+
key = send(primary_key)
|
48
|
+
key&.to_s
|
28
49
|
end
|
29
50
|
|
30
|
-
|
31
|
-
# Returns a cache key that can be used to identify this record.
|
32
|
-
#
|
33
|
-
# ==== Examples
|
51
|
+
# Returns a stable cache key that can be used to identify this record.
|
34
52
|
#
|
35
53
|
# Product.new.cache_key # => "products/new"
|
36
|
-
#
|
37
|
-
#
|
54
|
+
# Product.find(5).cache_key # => "products/5"
|
55
|
+
#
|
56
|
+
# If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier,
|
57
|
+
# the cache key will also include a version.
|
58
|
+
#
|
59
|
+
# Product.cache_versioning = false
|
60
|
+
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
|
38
61
|
#
|
39
62
|
def cache_key
|
40
63
|
case
|
41
64
|
when new_record? then
|
42
|
-
"#{
|
65
|
+
"#{model_name.cache_key}/new"
|
43
66
|
when ::ActiveRemote.config.default_cache_key_updated_at? && (timestamp = self[:updated_at]) then
|
44
67
|
timestamp = timestamp.utc.to_s(self.class.cache_timestamp_format)
|
45
|
-
"#{
|
68
|
+
"#{model_name.cache_key}/#{send(primary_key)}-#{timestamp}"
|
69
|
+
else
|
70
|
+
"#{model_name.cache_key}/#{send(primary_key)}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a cache key along with the version.
|
75
|
+
def cache_key_with_version
|
76
|
+
if (version = cache_version)
|
77
|
+
"#{cache_key}-#{version}"
|
46
78
|
else
|
47
|
-
|
79
|
+
cache_key
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a cache version that can be used together with the cache key to form
|
84
|
+
# a recyclable caching scheme. By default, the #updated_at column is used for the
|
85
|
+
# cache_version, but this method can be overwritten to return something else.
|
86
|
+
#
|
87
|
+
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
|
88
|
+
# +false+ (which it is by default until Rails 6.0).
|
89
|
+
def cache_version
|
90
|
+
if cache_versioning && (timestamp = try(:updated_at))
|
91
|
+
timestamp.utc.to_s(:usec)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
# Defines your model's +to_param+ method to generate "pretty" URLs
|
97
|
+
# using +method_name+, which can be any attribute or method that
|
98
|
+
# responds to +to_s+.
|
99
|
+
#
|
100
|
+
# class User < ActiveRecord::Base
|
101
|
+
# to_param :name
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# user = User.find_by(name: 'Fancy Pants')
|
105
|
+
# user.id # => 123
|
106
|
+
# user_path(user) # => "/users/123-fancy-pants"
|
107
|
+
#
|
108
|
+
# Values longer than 20 characters will be truncated. The value
|
109
|
+
# is truncated word by word.
|
110
|
+
#
|
111
|
+
# user = User.find_by(name: 'David Heinemeier Hansson')
|
112
|
+
# user.id # => 125
|
113
|
+
# user_path(user) # => "/users/125-david-heinemeier"
|
114
|
+
#
|
115
|
+
# Because the generated param begins with the record's +id+, it is
|
116
|
+
# suitable for passing to +find+. In a controller, for example:
|
117
|
+
#
|
118
|
+
# params[:id] # => "123-fancy-pants"
|
119
|
+
# User.find(params[:id]).id # => 123
|
120
|
+
def to_param(method_name = nil)
|
121
|
+
if method_name.nil?
|
122
|
+
super()
|
123
|
+
else
|
124
|
+
define_method :to_param do
|
125
|
+
if (default = super()) &&
|
126
|
+
(result = send(method_name).to_s).present? &&
|
127
|
+
(param = result.squish.parameterize.truncate(20, :separator => /-/, :omission => "")).present?
|
128
|
+
"#{default}-#{param}"
|
129
|
+
else
|
130
|
+
default
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
48
134
|
end
|
49
135
|
end
|
50
136
|
end
|