jets 0.7.1 → 0.8.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 +7 -0
- data/Gemfile.lock +1 -1
- data/lib/jets.rb +2 -0
- data/lib/jets/cfn/ship.rb +2 -0
- data/lib/jets/cfn/template_builders/base_child_builder.rb +14 -0
- data/lib/jets/cfn/template_builders/controller_builder.rb +4 -74
- data/lib/jets/cfn/template_builders/interface.rb +5 -0
- data/lib/jets/cfn/template_builders/job_builder.rb +1 -55
- data/lib/jets/cfn/template_builders/rule_builder.rb +7 -58
- data/lib/jets/cfn/template_mappers.rb +1 -4
- data/lib/jets/cfn/template_mappers/child_mapper.rb +1 -1
- data/lib/jets/cfn/template_mappers/gateway_resource_mapper.rb +5 -4
- data/lib/jets/commands/build.rb +2 -2
- data/lib/jets/commands/templates/skeleton/config/application.rb.tt +4 -1
- data/lib/jets/internal/app/jobs/jets/preheat_job.rb +2 -2
- data/lib/jets/job.rb +0 -1
- data/lib/jets/job/dsl.rb +52 -34
- data/lib/jets/lambda/dsl.rb +43 -1
- data/lib/jets/lambda/task.rb +2 -1
- data/lib/jets/pascalize.rb +26 -8
- data/lib/jets/resource.rb +7 -0
- data/lib/jets/resource/attributes.rb +46 -0
- data/lib/jets/resource/creator.rb +17 -0
- data/lib/jets/resource/permission.rb +43 -0
- data/lib/jets/resource/replacer.rb +40 -0
- data/lib/jets/resource/replacer/base.rb +98 -0
- data/lib/jets/resource/replacer/config_rule.rb +18 -0
- data/lib/jets/resource/route.rb +67 -0
- data/lib/jets/resource/route/attributes.rb +8 -0
- data/lib/jets/resource/route/cors.rb +60 -0
- data/lib/jets/rule.rb +0 -2
- data/lib/jets/rule/dsl.rb +71 -31
- data/lib/jets/version.rb +1 -1
- metadata +12 -8
- data/lib/jets/cfn/template_mappers/config_rule_mapper.rb +0 -34
- data/lib/jets/cfn/template_mappers/events_rule_mapper.rb +0 -40
- data/lib/jets/cfn/template_mappers/gateway_method_mapper.rb +0 -56
- data/lib/jets/job/task.rb +0 -17
- data/lib/jets/rule/aws_managed_rule.rb +0 -12
- data/lib/jets/rule/task.rb +0 -44
data/lib/jets/job.rb
CHANGED
data/lib/jets/job/dsl.rb
CHANGED
@@ -1,57 +1,75 @@
|
|
1
1
|
# Jets::Job::Base < Jets::Lambda::Functions
|
2
2
|
# Both Jets::Job::Base and Jets::Lambda::Functions have Dsl modules included.
|
3
3
|
# So the Jets::Job::Dsl overrides some of the Jets::Lambda::Functions behavior.
|
4
|
+
#
|
5
|
+
# Implements:
|
6
|
+
# default_associated_resource: must return @resources
|
4
7
|
module Jets::Job::Dsl
|
5
8
|
extend ActiveSupport::Concern
|
6
9
|
|
7
10
|
included do
|
8
11
|
class << self
|
9
12
|
def rate(expression)
|
10
|
-
|
13
|
+
update_properties(schedule_expression: "rate(#{expression})")
|
11
14
|
end
|
12
15
|
|
13
16
|
def cron(expression)
|
14
|
-
|
17
|
+
update_properties(schedule_expression: "cron(#{expression}")
|
15
18
|
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
def event_pattern(details={})
|
21
|
+
event_rule(event_pattern: details)
|
22
|
+
add_descriptions # useful: generic description in the Event Rule console
|
20
23
|
end
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
def add_descriptions
|
26
|
+
numbered_resources = []
|
27
|
+
n = 1
|
28
|
+
@resources.map do |definition|
|
29
|
+
logical_id = definition.keys.first
|
30
|
+
attributes = definition.values.first
|
31
|
+
attributes[:properties][:description] = "#{self.name} Event Rule #{n}"
|
32
|
+
numbered_resources << { "#{logical_id}" => attributes }
|
33
|
+
n += 1
|
34
|
+
end
|
35
|
+
@resources = numbered_resources
|
25
36
|
end
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def register_task(meth, lang=:ruby)
|
31
|
-
if @rate || @cron || @disable
|
32
|
-
# Job lambda function.
|
33
|
-
all_tasks[meth] = Jets::Job::Task.new(self.name, meth,
|
34
|
-
rate: @rate,
|
35
|
-
cron: @cron,
|
36
|
-
state: @state,
|
37
|
-
properties: @properties,
|
38
|
-
lang: lang)
|
39
|
-
true
|
40
|
-
else
|
41
|
-
task_name = "#{name}##{meth}" # IE: HardJob#dig
|
42
|
-
puts "[WARNING] #{task_name} created without a rate or cron expression. " \
|
43
|
-
"Add a rate or cron expression above the method definition if you want this method to be scheduled. " \
|
44
|
-
"If #{task_name} is not meant to be a scheduled lambda function, you can put the method under after a private keyword to get rid of this warning. " \
|
45
|
-
"#{task_name} defined at #{caller[1].inspect}."
|
46
|
-
false
|
47
|
-
end
|
48
|
-
# Done storing options, clear out for the next added method.
|
49
|
-
clear_properties
|
38
|
+
def default_associated_resource
|
39
|
+
event_rule
|
40
|
+
@resources # must return @resoures for update_properties
|
50
41
|
end
|
51
42
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
43
|
+
def event_rule(props={})
|
44
|
+
default_props = {
|
45
|
+
state: "ENABLED",
|
46
|
+
targets: [{
|
47
|
+
arn: "!GetAtt {namespace}LambdaFunction.Arn",
|
48
|
+
id: "{namespace}RuleTarget"
|
49
|
+
}]
|
50
|
+
}
|
51
|
+
properties = default_props.deep_merge(props)
|
52
|
+
|
53
|
+
resource("{namespace}EventsRule" => {
|
54
|
+
type: "AWS::Events::Rule",
|
55
|
+
properties: properties
|
56
|
+
})
|
57
|
+
|
58
|
+
add_logical_id_counter if @resources.size > 1
|
59
|
+
end
|
60
|
+
|
61
|
+
# Loop back through the resources and add a counter to the end of the id
|
62
|
+
# to handle multiple events.
|
63
|
+
# Then replace @resources entirely
|
64
|
+
def add_logical_id_counter
|
65
|
+
numbered_resources = []
|
66
|
+
n = 1
|
67
|
+
@resources.map do |definition|
|
68
|
+
logical_id = definition.keys.first
|
69
|
+
numbered_resources << { "#{logical_id}#{n}" => definition.values.first }
|
70
|
+
n += 1
|
71
|
+
end
|
72
|
+
@resources = numbered_resources
|
55
73
|
end
|
56
74
|
end
|
57
75
|
end
|
data/lib/jets/lambda/dsl.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# Other dsl that rely on this must implement
|
2
|
+
# default_associated_resource: must return @resources
|
1
3
|
module Jets::Lambda::Dsl
|
2
4
|
extend ActiveSupport::Concern
|
3
5
|
|
@@ -120,6 +122,44 @@ module Jets::Lambda::Dsl
|
|
120
122
|
!!(class_iam_policy || class_managed_iam_policy)
|
121
123
|
end
|
122
124
|
|
125
|
+
#############################
|
126
|
+
# Generic method that registers a resource to be associated with the Lambda function.
|
127
|
+
# In the future all DSL methods can lead here.
|
128
|
+
def resources(*definitions)
|
129
|
+
if definitions == [nil] # when resources called with no arguments
|
130
|
+
@resources || []
|
131
|
+
else
|
132
|
+
@resources ||= []
|
133
|
+
@resources += definitions
|
134
|
+
@resources.flatten!
|
135
|
+
end
|
136
|
+
end
|
137
|
+
alias_method :resource, :resources
|
138
|
+
|
139
|
+
# Main method that the convenience methods call for to create resources associated
|
140
|
+
# with the Lambda function. References the first resource and updates it inplace.
|
141
|
+
# Useful for associated resources that are meant to be declare and associated
|
142
|
+
# with only one Lambda function. Example:
|
143
|
+
#
|
144
|
+
# Config Rule <=> Lambda function is 1-to-1
|
145
|
+
#
|
146
|
+
# Note: This methods calls default_associated_resource. The inheriting DSL class
|
147
|
+
# must implement default_associated_resource. The default_associated_resource should
|
148
|
+
# wrap another method that is nicely name so that the nicely name method is
|
149
|
+
# available in the DSL. Example:
|
150
|
+
#
|
151
|
+
# def default_associated_resource
|
152
|
+
# config_rule
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
def update_properties(values={})
|
156
|
+
@resources ||= default_associated_resource
|
157
|
+
definition = @resources.first # singleton
|
158
|
+
attributes = definition.values.first
|
159
|
+
attributes[:properties].merge!(values)
|
160
|
+
@resources
|
161
|
+
end
|
162
|
+
|
123
163
|
# meth is a Symbol
|
124
164
|
def method_added(meth)
|
125
165
|
return if %w[initialize method_missing].include?(meth.to_s)
|
@@ -133,7 +173,8 @@ module Jets::Lambda::Dsl
|
|
133
173
|
# We adjust the class name when we build the functions later in
|
134
174
|
# FunctionContstructor#adjust_tasks.
|
135
175
|
all_tasks[meth] = Jets::Lambda::Task.new(self.name, meth,
|
136
|
-
|
176
|
+
resources: @resources, # associated resources
|
177
|
+
properties: @properties, # lambda function properties
|
137
178
|
iam_policy: @iam_policy,
|
138
179
|
managed_iam_policy: @managed_iam_policy,
|
139
180
|
lang: lang)
|
@@ -152,6 +193,7 @@ module Jets::Lambda::Dsl
|
|
152
193
|
end
|
153
194
|
|
154
195
|
def clear_properties
|
196
|
+
@resources = nil
|
155
197
|
@properties = nil
|
156
198
|
@iam_policy = nil
|
157
199
|
@managed_iam_policy = nil
|
data/lib/jets/lambda/task.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
class Jets::Lambda::Task
|
2
2
|
attr_accessor :class_name, :type
|
3
|
-
attr_reader :meth, :properties, :iam_policy, :managed_iam_policy, :lang
|
3
|
+
attr_reader :meth, :resources, :properties, :iam_policy, :managed_iam_policy, :lang
|
4
4
|
def initialize(class_name, meth, options={})
|
5
5
|
@class_name = class_name.to_s # use at EventsRuleMapper#full_task_name
|
6
6
|
@meth = meth
|
7
7
|
@options = options
|
8
8
|
@type = options[:type] || get_type # controller, job, or function
|
9
|
+
@resources = options[:resources] || {}
|
9
10
|
@properties = options[:properties] || {}
|
10
11
|
@iam_policy = options[:iam_policy]
|
11
12
|
@managed_iam_policy = options[:managed_iam_policy]
|
data/lib/jets/pascalize.rb
CHANGED
@@ -4,14 +4,14 @@ module Jets
|
|
4
4
|
# Specialized pascalize that will not pascalize keys under the
|
5
5
|
# Variables part of the hash structure.
|
6
6
|
# Based on: https://stackoverflow.com/questions/8706930/converting-nested-hash-keys-from-camelcase-to-snake-case-in-ruby
|
7
|
-
def pascalize(value,
|
7
|
+
def pascalize(value, parent_keys=[])
|
8
8
|
case value
|
9
9
|
when Array
|
10
10
|
value.map { |v| pascalize(v) }
|
11
11
|
when Hash
|
12
12
|
initializer = value.map do |k, v|
|
13
|
-
new_key = pascal_key(k,
|
14
|
-
[new_key, pascalize(v, new_key)]
|
13
|
+
new_key = pascal_key(k, parent_keys)
|
14
|
+
[new_key, pascalize(v, parent_keys+[new_key])]
|
15
15
|
end
|
16
16
|
Hash[initializer]
|
17
17
|
else
|
@@ -19,14 +19,32 @@ module Jets
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
def pascal_key(k,
|
23
|
-
|
24
|
-
|
22
|
+
def pascal_key(k, parent_keys=[])
|
23
|
+
k = k.to_s
|
24
|
+
if parent_keys.include?("Variables") # do not pascalize keys anything under Variables
|
25
|
+
k # pass through untouch
|
26
|
+
elsif parent_keys.include?("ResponseParameters")
|
27
|
+
k # pass through untouch
|
28
|
+
elsif k.include?('-') || k.include?('/')
|
29
|
+
k # pass through untouch
|
30
|
+
elsif parent_keys.last == "EventPattern" # top-level
|
31
|
+
k.dasherize
|
32
|
+
elsif parent_keys.include?("EventPattern")
|
33
|
+
# any keys at 2nd level under EventPattern will be camelize
|
34
|
+
new_k = k.camelize # an earlier pascalize has made the first char upcase
|
35
|
+
# so we need to downcase it again
|
36
|
+
first_char = new_k[0..0].downcase
|
37
|
+
new_k[0] = first_char
|
38
|
+
new_k
|
25
39
|
else
|
26
|
-
k
|
27
|
-
k.slice(0,1).capitalize + k.slice(1..-1) # capitalize first letter only
|
40
|
+
pascalize_string(k)
|
28
41
|
end
|
29
42
|
end
|
43
|
+
|
44
|
+
def pascalize_string(s)
|
45
|
+
s = s.to_s.camelize
|
46
|
+
s.slice(0,1).capitalize + s.slice(1..-1) # capitalize first letter only
|
47
|
+
end
|
30
48
|
end
|
31
49
|
end
|
32
50
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Jets::Resource
|
2
|
+
class Attributes
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
def initialize(data, task, replacements={})
|
6
|
+
@data = data
|
7
|
+
@task = task
|
8
|
+
@replacements = replacements
|
9
|
+
end
|
10
|
+
|
11
|
+
def logical_id
|
12
|
+
id = @data.keys.first
|
13
|
+
# replace possible {namespace} in the logical id
|
14
|
+
id = replacer.replace_value(id)
|
15
|
+
Jets::Pascalize.pascalize_string(id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def type
|
19
|
+
attributes['Type']
|
20
|
+
end
|
21
|
+
|
22
|
+
def properties
|
23
|
+
attributes['Properties']
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes
|
27
|
+
attributes = @data.values.first
|
28
|
+
attributes = replacer.replace_placeholders(attributes, @replacements)
|
29
|
+
Jets::Pascalize.pascalize(attributes)
|
30
|
+
end
|
31
|
+
|
32
|
+
def replacer
|
33
|
+
# Use raw @data to avoid infinite loop from using attributes
|
34
|
+
attributes = Jets::Pascalize.pascalize(@data.values.first)
|
35
|
+
type = attributes['Type']
|
36
|
+
replacer_class = Replacer.lookup(type)
|
37
|
+
replacer_class.new(@task)
|
38
|
+
end
|
39
|
+
memoize :replacer
|
40
|
+
|
41
|
+
def permission
|
42
|
+
Permission.new(@task, self)
|
43
|
+
end
|
44
|
+
memoize :permission
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Jets::Resource
|
2
|
+
class Creator
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
def initialize(definition, task)
|
6
|
+
@definition = definition
|
7
|
+
@task = task # task that the definition belongs to
|
8
|
+
end
|
9
|
+
|
10
|
+
# Template snippet that gets injected into the CloudFormation template.
|
11
|
+
def attributes
|
12
|
+
Attributes.new(@definition, @task)
|
13
|
+
end
|
14
|
+
alias_method :resource, :attributes
|
15
|
+
memoize :attributes
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Jets::Resource
|
2
|
+
class Permission
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
def initialize(task, resource)
|
6
|
+
@task = task
|
7
|
+
@resource_attributes = resource
|
8
|
+
end
|
9
|
+
|
10
|
+
def attributes
|
11
|
+
logical_id = "{namespace}Permission"
|
12
|
+
md = @resource_attributes.logical_id.match(/(\d+)/)
|
13
|
+
if md
|
14
|
+
counter = md[1]
|
15
|
+
end
|
16
|
+
logical_id = [logical_id, counter].compact.join('')
|
17
|
+
|
18
|
+
attributes = {
|
19
|
+
logical_id => {
|
20
|
+
type: "AWS::Lambda::Permission",
|
21
|
+
properties: {
|
22
|
+
function_name: "!GetAtt {namespace}LambdaFunction.Arn",
|
23
|
+
action: "lambda:InvokeFunction",
|
24
|
+
principal: principal,
|
25
|
+
source_arn: source_arn,
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
Attributes.new(attributes, @task)
|
30
|
+
end
|
31
|
+
memoize :attributes
|
32
|
+
|
33
|
+
# Auto-detect principal from the associated resources.
|
34
|
+
def principal
|
35
|
+
Replacer.principal_map(@resource_attributes.type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def source_arn
|
39
|
+
default_arn = "!GetAtt #{@resource_attributes.logical_id}.Arn"
|
40
|
+
Replacer.source_arn_map(@resource_attributes.type) || default_arn
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Jets::Resource
|
2
|
+
module Replacer
|
3
|
+
autoload :Base, 'jets/resource/replacer/base'
|
4
|
+
autoload :ConfigRule, 'jets/resource/replacer/config_rule'
|
5
|
+
# TODO: handle autoloading for plugins
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def lookup(type)
|
9
|
+
klass = replacer_map[type] || "Jets::Resource::Replacer::Base"
|
10
|
+
klass.constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
# Maps
|
14
|
+
# TODO: get rid of this map, and use a convention
|
15
|
+
# * connect a plugin to figure out interface.
|
16
|
+
# * add ability to explicitly override principal and source_arn.
|
17
|
+
def replacer_map
|
18
|
+
{
|
19
|
+
"AWS::Config::ConfigRule" => "Jets::Resource::Replacer::ConfigRule"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Examples:
|
24
|
+
# "AWS::Events::Rule" => "events.amazonaws.com",
|
25
|
+
# "AWS::Config::ConfigRule" => "config.amazonaws.com",
|
26
|
+
# "AWS::ApiGateway::Method" => "apigateway.amazonaws.com"
|
27
|
+
def principal_map(type)
|
28
|
+
service = type.split('::')[1].downcase
|
29
|
+
"#{service}.amazonaws.com"
|
30
|
+
end
|
31
|
+
|
32
|
+
def source_arn_map(type)
|
33
|
+
map = {
|
34
|
+
"AWS::ApiGateway::Method" => "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*",
|
35
|
+
}
|
36
|
+
map[type]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Jets::Resource::Replacer
|
2
|
+
class Base
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
def initialize(task)
|
6
|
+
@task = task
|
7
|
+
@app_class = task.class_name.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
# Replace placeholder hash values with replacements. This does a deep replacement
|
11
|
+
# to the hash values. The replacement "key" is the string value within the value.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# attributes = {whatever: "foo REPLACE_KEY bar" }
|
16
|
+
# replace_placeholders(attributes, REPLACE_KEY: "blah:arn")
|
17
|
+
# => {whatever: "foo blah:arn bar" }
|
18
|
+
#
|
19
|
+
# Also, we always replace the special {namespace} value in the hash values. Example:
|
20
|
+
#
|
21
|
+
# attributes = {whatever: "{namespace}LambdaFunction" }
|
22
|
+
# replace_placeholders(attributes, {})
|
23
|
+
# => {whatever: "foo PostsControllerIndexLambdaFunction bar" }
|
24
|
+
#
|
25
|
+
def replace_placeholders(attributes, replacements={})
|
26
|
+
update_values(attributes, replacements)
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_values(original, replacements={})
|
30
|
+
case original
|
31
|
+
when Array
|
32
|
+
original.map { |v| update_values(v, replacements) }
|
33
|
+
when Hash
|
34
|
+
initializer = original.map do |k, v|
|
35
|
+
[k, update_values(v, replacements)]
|
36
|
+
end
|
37
|
+
Hash[initializer]
|
38
|
+
else
|
39
|
+
replace_value(original, replacements)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def replace_value(text, replacements={})
|
44
|
+
text = text.to_s # normalize to String
|
45
|
+
# custom replacements
|
46
|
+
replacements.each do |k,v|
|
47
|
+
text = text.gsub(k.to_s, v)
|
48
|
+
end
|
49
|
+
# Values to always replace
|
50
|
+
text = replace_core_values(text)
|
51
|
+
text
|
52
|
+
end
|
53
|
+
|
54
|
+
# Values to always replace.
|
55
|
+
def replace_core_values(text)
|
56
|
+
text = text.gsub('{namespace}', namespace) # always replace namespace
|
57
|
+
core_replacements.each do |k,v|
|
58
|
+
text = text.gsub("{#{k}}", v)
|
59
|
+
end
|
60
|
+
text
|
61
|
+
end
|
62
|
+
|
63
|
+
# Meant to beoverriden by different resource types in the child class.
|
64
|
+
# These values replace the variables in the resource template.
|
65
|
+
#
|
66
|
+
# Example:
|
67
|
+
#
|
68
|
+
# In child class:
|
69
|
+
#
|
70
|
+
# def core_replacements
|
71
|
+
# { config_rule_name: "my-config-rule" }
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# And we declare these properties in the resource:
|
75
|
+
#
|
76
|
+
# properties: {
|
77
|
+
# config_rule_name: "{config_rule_name}",
|
78
|
+
# ...
|
79
|
+
#
|
80
|
+
# The replacements result in:
|
81
|
+
#
|
82
|
+
# properties: {
|
83
|
+
# config_rule_name: "my-config-rule",
|
84
|
+
# ...
|
85
|
+
#
|
86
|
+
def core_replacements
|
87
|
+
{}
|
88
|
+
end
|
89
|
+
|
90
|
+
# Full camelized namespace
|
91
|
+
# Example: HardJobDig, PostsControllerIndex, SleepJobPerform
|
92
|
+
def namespace
|
93
|
+
class_name = @task.class_name.gsub('::','')
|
94
|
+
function_name = @task.meth.to_s.camelize
|
95
|
+
"#{class_name}#{function_name}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|