kameleoon-client-ruby 1.1.2 → 2.0.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/lib/kameleoon/client.rb +456 -390
- data/lib/kameleoon/configuration/experiment.rb +42 -0
- data/lib/kameleoon/configuration/feature_flag.rb +41 -0
- data/lib/kameleoon/configuration/feature_flag_v2.rb +30 -0
- data/lib/kameleoon/configuration/rule.rb +45 -0
- data/lib/kameleoon/configuration/variable.rb +23 -0
- data/lib/kameleoon/configuration/variation.rb +31 -0
- data/lib/kameleoon/configuration/variation_exposition.rb +23 -0
- data/lib/kameleoon/cookie.rb +11 -4
- data/lib/kameleoon/data.rb +36 -22
- data/lib/kameleoon/exceptions.rb +46 -23
- data/lib/kameleoon/factory.rb +21 -18
- data/lib/kameleoon/request.rb +14 -13
- data/lib/kameleoon/storage/variation_storage.rb +42 -0
- data/lib/kameleoon/storage/visitor_variation.rb +20 -0
- data/lib/kameleoon/targeting/condition.rb +15 -3
- data/lib/kameleoon/targeting/condition_factory.rb +9 -2
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +20 -36
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +29 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +44 -0
- data/lib/kameleoon/targeting/models.rb +36 -36
- data/lib/kameleoon/utils.rb +4 -1
- data/lib/kameleoon/version.rb +4 -2
- metadata +13 -3
- data/lib/kameleoon/query_graphql.rb +0 -76
@@ -1,11 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kameleoon
|
2
|
-
|
4
|
+
# @api private
|
3
5
|
module Targeting
|
4
6
|
class Condition
|
5
7
|
attr_accessor :type, :include
|
6
8
|
|
7
|
-
def initialize(
|
8
|
-
|
9
|
+
def initialize(json_condition)
|
10
|
+
if json_condition['targetingType'].nil?
|
11
|
+
raise Exception::NotFoundError.new('targetingType'), 'targetingType missed'
|
12
|
+
end
|
13
|
+
|
14
|
+
@type = json_condition['targetingType']
|
15
|
+
|
16
|
+
if json_condition['include'].nil? && json_condition['isInclude'].nil?
|
17
|
+
raise Exception::NotFoundError.new('include / isInclude missed'), 'include / isInclude missed'
|
18
|
+
end
|
19
|
+
|
20
|
+
@include = json_condition['include'] || json_condition['isInclude']
|
9
21
|
end
|
10
22
|
|
11
23
|
def check(conditions)
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'kameleoon/targeting/conditions/custom_datum'
|
2
|
+
require 'kameleoon/targeting/conditions/target_experiment'
|
3
|
+
require 'kameleoon/targeting/conditions/exclusive_experiment'
|
2
4
|
|
3
5
|
module Kameleoon
|
4
6
|
#@api private
|
@@ -6,11 +8,16 @@ module Kameleoon
|
|
6
8
|
module ConditionFactory
|
7
9
|
def get_condition(condition_json)
|
8
10
|
condition = nil
|
9
|
-
|
11
|
+
case condition_json['targetingType']
|
12
|
+
when ConditionType::CUSTOM_DATUM.to_s
|
10
13
|
condition = CustomDatum.new(condition_json)
|
14
|
+
when ConditionType::TARGET_EXPERIMENT.to_s
|
15
|
+
condition = TargetExperiment.new(condition_json)
|
16
|
+
when ConditionType::EXCLUSIVE_EXPERIMENT.to_s
|
17
|
+
condition = ExclusiveExperiment.new(condition_json)
|
11
18
|
end
|
12
19
|
condition
|
13
20
|
end
|
14
21
|
end
|
15
22
|
end
|
16
|
-
end
|
23
|
+
end
|
@@ -1,34 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'kameleoon/targeting/condition'
|
2
4
|
require 'kameleoon/exceptions'
|
3
5
|
|
4
6
|
module Kameleoon
|
5
|
-
#@api private
|
6
7
|
module Targeting
|
8
|
+
# CustomDatum represents an instance of Custom Data condition from back office
|
7
9
|
class CustomDatum < Condition
|
8
10
|
include Kameleoon::Exception
|
9
11
|
|
10
|
-
attr_accessor :index, :operator, :value
|
11
12
|
def initialize(json_condition)
|
12
|
-
|
13
|
-
raise Exception::NotFoundError.new('customDataIndex')
|
14
|
-
end
|
13
|
+
super(json_condition)
|
15
14
|
@index = json_condition['customDataIndex']
|
16
15
|
|
17
16
|
if json_condition['valueMatchType'].nil?
|
18
|
-
raise Exception::NotFoundError.new('valueMatchType')
|
17
|
+
raise Exception::NotFoundError.new('valueMatchType'), 'valueMatchType missed'
|
19
18
|
end
|
19
|
+
|
20
20
|
@operator = json_condition['valueMatchType']
|
21
21
|
|
22
|
-
# if json_condition['value'].nil?
|
23
|
-
# raise Exception::NotFoundError.new('value')
|
24
|
-
# end
|
25
22
|
@value = json_condition['value']
|
26
23
|
|
27
24
|
@type = ConditionType::CUSTOM_DATUM
|
28
25
|
|
29
26
|
if json_condition['include'].nil? && json_condition['isInclude'].nil?
|
30
|
-
raise Exception::NotFoundError.new('include / isInclude missed')
|
27
|
+
raise Exception::NotFoundError.new('include / isInclude missed'), 'include / isInclude missed'
|
31
28
|
end
|
29
|
+
|
32
30
|
@include = json_condition['include'] || json_condition['isInclude']
|
33
31
|
end
|
34
32
|
|
@@ -40,43 +38,29 @@ module Kameleoon
|
|
40
38
|
else
|
41
39
|
case @operator
|
42
40
|
when Operator::MATCH.to_s
|
43
|
-
|
44
|
-
is_targeted = true
|
45
|
-
end
|
41
|
+
is_targeted = !Regexp.new(@value.to_s).match(custom_data.value.to_s).nil?
|
46
42
|
when Operator::CONTAINS.to_s
|
47
|
-
|
48
|
-
is_targeted = true
|
49
|
-
end
|
43
|
+
is_targeted = custom_data.value.to_s.include? @value
|
50
44
|
when Operator::EXACT.to_s
|
51
|
-
|
52
|
-
is_targeted = true
|
53
|
-
end
|
45
|
+
is_targeted = custom_data.value.to_s == @value.to_s
|
54
46
|
when Operator::EQUAL.to_s
|
55
|
-
|
56
|
-
is_targeted = true
|
57
|
-
end
|
47
|
+
is_targeted = custom_data.value.to_f == @value.to_f
|
58
48
|
when Operator::GREATER.to_s
|
59
|
-
|
60
|
-
is_targeted = true
|
61
|
-
end
|
49
|
+
is_targeted = custom_data.value.to_f > @value.to_f
|
62
50
|
when Operator::LOWER.to_s
|
63
|
-
|
64
|
-
is_targeted = true
|
65
|
-
end
|
51
|
+
is_targeted = custom_data.value.to_f < @value.to_f
|
66
52
|
when Operator::IS_TRUE.to_s
|
67
|
-
|
68
|
-
is_targeted = true
|
69
|
-
end
|
53
|
+
is_targeted = custom_data.value == 'true'
|
70
54
|
when Operator::IS_FALSE.to_s
|
71
|
-
|
72
|
-
|
73
|
-
|
55
|
+
is_targeted = custom_data.value == 'false'
|
56
|
+
when Operator::AMONG_VALUES.to_s
|
57
|
+
is_targeted = @value.scan(/"([^"]*)"/).flatten.include?(custom_data.value)
|
74
58
|
else
|
75
|
-
raise KameleoonError.new("Undefined operator "
|
59
|
+
raise KameleoonError.new("Undefined operator #{@operator}"), "Undefined operator #{@operator}"
|
76
60
|
end
|
77
61
|
end
|
78
62
|
is_targeted
|
79
63
|
end
|
80
64
|
end
|
81
65
|
end
|
82
|
-
end
|
66
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kameleoon/targeting/condition'
|
4
|
+
require 'kameleoon/exceptions'
|
5
|
+
|
6
|
+
module Kameleoon
|
7
|
+
# @api private
|
8
|
+
module Targeting
|
9
|
+
# ExclusiveExperiment represents an instance of Exclusive Experiment condition in user account
|
10
|
+
class ExclusiveExperiment < Condition
|
11
|
+
include Kameleoon::Exception
|
12
|
+
|
13
|
+
def initialize(json_condition)
|
14
|
+
if json_condition['targetingType'].nil?
|
15
|
+
raise Exception::NotFoundError.new('targetingType'), 'targetingType missed'
|
16
|
+
end
|
17
|
+
|
18
|
+
@type = json_condition['targetingType']
|
19
|
+
@include = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def check(data)
|
23
|
+
experiment_id = data.experiment_id
|
24
|
+
storage = data.storage
|
25
|
+
storage.nil? || storage.empty? || (storage.length == 1 && !storage[experiment_id].nil?)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kameleoon/targeting/condition'
|
4
|
+
require 'kameleoon/exceptions'
|
5
|
+
|
6
|
+
module Kameleoon
|
7
|
+
# @api private
|
8
|
+
module Targeting
|
9
|
+
# TargetExperiment represents an instance of Experiment condition in user account
|
10
|
+
class TargetExperiment < Condition
|
11
|
+
include Kameleoon::Exception
|
12
|
+
|
13
|
+
def initialize(json_condition)
|
14
|
+
super(json_condition)
|
15
|
+
|
16
|
+
if json_condition['experiment'].nil?
|
17
|
+
raise Exception::NotFoundError.new('experiment'), 'experiment missed'
|
18
|
+
end
|
19
|
+
|
20
|
+
@experiment = json_condition['experiment']
|
21
|
+
|
22
|
+
if json_condition['variationMatchType'].nil?
|
23
|
+
raise Exception::NotFoundError.new('variationMatchType'), 'variationMatchType missed'
|
24
|
+
end
|
25
|
+
|
26
|
+
@operator = json_condition['variationMatchType']
|
27
|
+
@variation = json_condition['variation']
|
28
|
+
end
|
29
|
+
|
30
|
+
def check(variation_storage)
|
31
|
+
is_targeted = false
|
32
|
+
variation_storage_exist = !variation_storage.nil? && !variation_storage.empty?
|
33
|
+
saved_variation = variation_storage[@experiment] unless variation_storage.nil?
|
34
|
+
case @operator
|
35
|
+
when Operator::EXACT.to_s
|
36
|
+
is_targeted = variation_storage_exist && saved_variation == @variation
|
37
|
+
when Operator::ANY.to_s
|
38
|
+
is_targeted = variation_storage_exist
|
39
|
+
end
|
40
|
+
is_targeted
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,15 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'kameleoon/targeting/tree_builder'
|
2
4
|
require 'kameleoon/exceptions'
|
3
5
|
|
4
6
|
module Kameleoon
|
5
|
-
|
7
|
+
# @api private
|
6
8
|
module Targeting
|
7
9
|
class Segment
|
8
10
|
include TreeBuilder
|
9
11
|
attr_accessor :id, :tree
|
10
12
|
def to_s
|
11
|
-
print("\nSegment id:
|
12
|
-
print("\n")
|
13
|
+
print("\nSegment id: #{@id}\n")
|
13
14
|
@tree.to_s
|
14
15
|
end
|
15
16
|
|
@@ -18,16 +19,16 @@ module Kameleoon
|
|
18
19
|
if args.length == 1
|
19
20
|
hash = args.first
|
20
21
|
if hash.nil?
|
21
|
-
raise Kameleoon::Exception::NotFound.new(
|
22
|
+
raise Kameleoon::Exception::NotFound.new('arguments for segment'), 'arguments for segment'
|
22
23
|
end
|
23
|
-
if hash[
|
24
|
-
raise Kameleoon::Exception::NotFound.new(
|
24
|
+
if hash['id'].nil?
|
25
|
+
raise Kameleoon::Exception::NotFound.new('id'), 'id'
|
25
26
|
end
|
26
|
-
@id = hash[
|
27
|
-
if hash[
|
28
|
-
raise Kameleoon::Exception::NotFound.new(hash[
|
27
|
+
@id = hash['id'].to_i
|
28
|
+
if hash['conditionsData'].nil?
|
29
|
+
raise Kameleoon::Exception::NotFound.new(hash['conditionsData']), 'hash[\'conditionsData\']'
|
29
30
|
end
|
30
|
-
@tree = create_tree(hash[
|
31
|
+
@tree = create_tree(hash['conditionsData'])
|
31
32
|
elsif args.length == 2
|
32
33
|
@id = args[0]
|
33
34
|
@tree = args[1]
|
@@ -49,17 +50,14 @@ module Kameleoon
|
|
49
50
|
attr_accessor :or_operator, :left_child, :right_child, :condition
|
50
51
|
|
51
52
|
def to_s
|
52
|
-
print("or_operator:
|
53
|
-
print("
|
54
|
-
print("condition: " + @condition.to_s)
|
53
|
+
print("or_operator: #{@or_operator}\n")
|
54
|
+
print("condition: #{@condition}")
|
55
55
|
unless @left_child.nil?
|
56
|
-
print(
|
57
|
-
print("Left child:\n ")
|
56
|
+
print('\nLeft child:\n ')
|
58
57
|
@left_child.to_s
|
59
58
|
end
|
60
59
|
unless @right_child.nil?
|
61
|
-
print(
|
62
|
-
print("right child:\n ")
|
60
|
+
print('\nright child:\n ')
|
63
61
|
@right_child.to_s
|
64
62
|
end
|
65
63
|
end
|
@@ -125,37 +123,39 @@ module Kameleoon
|
|
125
123
|
if condition.nil?
|
126
124
|
is_targeted = true
|
127
125
|
else
|
128
|
-
is_targeted = condition.check(datas)
|
126
|
+
is_targeted = condition.check(datas.call(condition.type))
|
129
127
|
unless condition.include
|
130
|
-
if is_targeted.nil?
|
131
|
-
|
132
|
-
|
133
|
-
is_targeted = !is_targeted
|
134
|
-
end
|
128
|
+
return true if is_targeted.nil?
|
129
|
+
|
130
|
+
is_targeted = !is_targeted
|
135
131
|
end
|
136
132
|
end
|
137
|
-
Marshal.load(Marshal.dump(is_targeted)) #Deep copy
|
133
|
+
Marshal.load(Marshal.dump(is_targeted)) # Deep copy
|
138
134
|
end
|
139
135
|
end
|
140
136
|
|
141
137
|
module DataType
|
142
|
-
CUSTOM =
|
138
|
+
CUSTOM = 'CUSTOM'
|
143
139
|
end
|
144
140
|
|
145
141
|
module ConditionType
|
146
|
-
CUSTOM_DATUM =
|
142
|
+
CUSTOM_DATUM = 'CUSTOM_DATUM'
|
143
|
+
TARGET_EXPERIMENT = 'TARGET_EXPERIMENT'
|
144
|
+
EXCLUSIVE_EXPERIMENT = 'EXCLUSIVE_EXPERIMENT'
|
147
145
|
end
|
148
146
|
|
149
147
|
module Operator
|
150
|
-
UNDEFINED =
|
151
|
-
CONTAINS =
|
152
|
-
EXACT =
|
153
|
-
MATCH =
|
154
|
-
LOWER =
|
155
|
-
EQUAL =
|
156
|
-
GREATER =
|
157
|
-
IS_TRUE =
|
158
|
-
IS_FALSE =
|
148
|
+
UNDEFINED = 'UNDEFINED'
|
149
|
+
CONTAINS = 'CONTAINS'
|
150
|
+
EXACT = 'EXACT'
|
151
|
+
MATCH = 'REGULAR_EXPRESSION'
|
152
|
+
LOWER = 'LOWER'
|
153
|
+
EQUAL = 'EQUAL'
|
154
|
+
GREATER = 'GREATER'
|
155
|
+
IS_TRUE = 'TRUE'
|
156
|
+
IS_FALSE = 'FALSE'
|
157
|
+
AMONG_VALUES = 'AMONG_VALUES'
|
158
|
+
ANY = 'ANY'
|
159
159
|
end
|
160
160
|
end
|
161
|
-
end
|
161
|
+
end
|
data/lib/kameleoon/utils.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kameleoon
|
4
|
+
# Utils is a helper module for project
|
2
5
|
module Utils
|
3
6
|
ALPHA_NUMERIC_CHARS = 'abcdef0123456789'
|
4
7
|
|
@@ -10,4 +13,4 @@ module Kameleoon
|
|
10
13
|
(1..length).map { ALPHA_NUMERIC_CHARS[rand(ALPHA_NUMERIC_CHARS.length)] }.join
|
11
14
|
end
|
12
15
|
end
|
13
|
-
end
|
16
|
+
end
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kameleoon-client-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kameleoon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: em-http-request
|
@@ -62,15 +62,25 @@ files:
|
|
62
62
|
- README.md
|
63
63
|
- lib/kameleoon.rb
|
64
64
|
- lib/kameleoon/client.rb
|
65
|
+
- lib/kameleoon/configuration/experiment.rb
|
66
|
+
- lib/kameleoon/configuration/feature_flag.rb
|
67
|
+
- lib/kameleoon/configuration/feature_flag_v2.rb
|
68
|
+
- lib/kameleoon/configuration/rule.rb
|
69
|
+
- lib/kameleoon/configuration/variable.rb
|
70
|
+
- lib/kameleoon/configuration/variation.rb
|
71
|
+
- lib/kameleoon/configuration/variation_exposition.rb
|
65
72
|
- lib/kameleoon/cookie.rb
|
66
73
|
- lib/kameleoon/data.rb
|
67
74
|
- lib/kameleoon/exceptions.rb
|
68
75
|
- lib/kameleoon/factory.rb
|
69
|
-
- lib/kameleoon/query_graphql.rb
|
70
76
|
- lib/kameleoon/request.rb
|
77
|
+
- lib/kameleoon/storage/variation_storage.rb
|
78
|
+
- lib/kameleoon/storage/visitor_variation.rb
|
71
79
|
- lib/kameleoon/targeting/condition.rb
|
72
80
|
- lib/kameleoon/targeting/condition_factory.rb
|
73
81
|
- lib/kameleoon/targeting/conditions/custom_datum.rb
|
82
|
+
- lib/kameleoon/targeting/conditions/exclusive_experiment.rb
|
83
|
+
- lib/kameleoon/targeting/conditions/target_experiment.rb
|
74
84
|
- lib/kameleoon/targeting/models.rb
|
75
85
|
- lib/kameleoon/targeting/tree_builder.rb
|
76
86
|
- lib/kameleoon/utils.rb
|
@@ -1,76 +0,0 @@
|
|
1
|
-
module Kameleoon
|
2
|
-
module Query
|
3
|
-
|
4
|
-
def self.query_experiments(site_code)
|
5
|
-
'{
|
6
|
-
"operationName": "getExperiments",
|
7
|
-
"query": "query getExperiments($first: Int, $after: String, $filter: FilteringExpression, $sort: [SortingParameter!]) { experiments(first: $first, after: $after, filter: $filter, sort: $sort) { edges { node { id name type site { id code isKameleoonEnabled } status variations { id customJson } deviations { variationId value } respoolTime {variationId value } segment { id name conditionsData { firstLevelOrOperators firstLevel { orOperators conditions { targetingType isInclude ... on CustomDataTargetingCondition { customDataIndex value valueMatchType } } } } } __typename } __typename } pageInfo { endCursor hasNextPage __typename } totalCount __typename } }",
|
8
|
-
"variables": {
|
9
|
-
"filter": {
|
10
|
-
"and": [{
|
11
|
-
"condition": {
|
12
|
-
"field": "status",
|
13
|
-
"operator": "IN",
|
14
|
-
"parameters": ["ACTIVE", "DEVIATED", "USED_AS_PERSONALIZATION"]
|
15
|
-
}
|
16
|
-
},
|
17
|
-
{
|
18
|
-
"condition": {
|
19
|
-
"field": "type",
|
20
|
-
"operator": "IN",
|
21
|
-
"parameters": ["SERVER_SIDE", "HYBRID"]
|
22
|
-
}
|
23
|
-
},
|
24
|
-
{
|
25
|
-
"condition": {
|
26
|
-
"field": "siteCode",
|
27
|
-
"operator": "IN",
|
28
|
-
"parameters": ["' + site_code + '"]
|
29
|
-
}
|
30
|
-
}]
|
31
|
-
},
|
32
|
-
"sort": [{
|
33
|
-
"field": "id",
|
34
|
-
"direction": "ASC"
|
35
|
-
}]
|
36
|
-
}
|
37
|
-
}'
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.query_feature_flags(site_code, environment)
|
41
|
-
'{
|
42
|
-
"operationName": "getFeatureFlags",
|
43
|
-
"query": "query getFeatureFlags($first: Int, $after: String, $filter: FilteringExpression, $sort: [SortingParameter!]) { featureFlags(first: $first, after: $after, filter: $filter, sort: $sort) { edges { node { id name site { id code isKameleoonEnabled } bypassDeviation status variations { id customJson } respoolTime { variationId value } expositionRate identificationKey featureFlagSdkLanguageType featureStatus schedules { dateStart dateEnd } segment { id name conditionsData { firstLevelOrOperators firstLevel { orOperators conditions { targetingType isInclude ... on CustomDataTargetingCondition { customDataIndex value valueMatchType } } } } } __typename } __typename } pageInfo { endCursor hasNextPage __typename } totalCount __typename } }",
|
44
|
-
"variables": {
|
45
|
-
"filter": {
|
46
|
-
"and": [{
|
47
|
-
"condition": {
|
48
|
-
"field": "featureStatus",
|
49
|
-
"operator": "IN",
|
50
|
-
"parameters": ["ACTIVATED", "SCHEDULED", "DEACTIVATED"]
|
51
|
-
}
|
52
|
-
},
|
53
|
-
{
|
54
|
-
"condition": {
|
55
|
-
"field": "siteCode",
|
56
|
-
"operator": "IN",
|
57
|
-
"parameters": ["' + site_code + '"]
|
58
|
-
}
|
59
|
-
},
|
60
|
-
{
|
61
|
-
"condition": {
|
62
|
-
"field": "environment.key",
|
63
|
-
"operator": "IN",
|
64
|
-
"parameters": ["' + environment + '"]
|
65
|
-
}
|
66
|
-
}]
|
67
|
-
},
|
68
|
-
"sort": [{
|
69
|
-
"field": "id",
|
70
|
-
"direction": "ASC"
|
71
|
-
}]
|
72
|
-
}
|
73
|
-
}'
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|