jets 1.5.9 → 1.5.10
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 +3 -0
- data/Gemfile.lock +3 -3
- data/lib/jets/commands/new.rb +1 -0
- data/lib/jets/resource/api_gateway/base_path/role.rb +1 -1
- data/lib/jets/resource/api_gateway/rest_api/logical_id.rb +0 -1
- data/lib/jets/resource/api_gateway/rest_api/routes/change.rb +6 -108
- data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +100 -0
- data/lib/jets/resource/api_gateway/rest_api/routes/change/to.rb +29 -0
- data/lib/jets/resource/api_gateway/rest_api/routes/change/variable.rb +39 -0
- data/lib/jets/resource/api_gateway/rest_api/routes/collision.rb +15 -13
- data/lib/jets/router.rb +3 -2
- data/lib/jets/version.rb +1 -1
- metadata +5 -3
- data/lib/jets/resource/api_gateway/rest_api/routes/base.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39b6cde7b66e43e4c253e0f055952680298287d9e7c534c4a465ca075b3dfb62
|
4
|
+
data.tar.gz: d3312104416979ca67bd31cebd3a7c2ba604185349c2c368de438d50a68914ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b670d6b6a2c0189fd608d4a7ba61a02206a4ddf78d2c8eda2c4f06d8c4c1beddb2588b417211e697ea0b24f8c426975c9ab08819a56fc3da41f6c959fb272ca
|
7
|
+
data.tar.gz: c84383ea55a03f9b0576fb5ea7c4a895a9e5ae818bd79df008a9e7f621f93ce2d5e4e189d7f4aa9a9a2c0fef12ae7a668747308b263785de0b8d0c46da8330f7
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
|
5
5
|
|
6
|
+
## [1.5.10]
|
7
|
+
- #157 Improve Route Change Detection: Path Variables
|
8
|
+
|
6
9
|
## [1.5.9]
|
7
10
|
- #154 from tongueroo/variable-collision raise error on multiple sibling variable paths collision
|
8
11
|
- #156 from konnected-io/master: don't prewarm jobs, only prewarm controllers
|
data/Gemfile.lock
CHANGED
@@ -11,7 +11,7 @@ GIT
|
|
11
11
|
PATH
|
12
12
|
remote: .
|
13
13
|
specs:
|
14
|
-
jets (1.5.
|
14
|
+
jets (1.5.10)
|
15
15
|
activerecord (~> 5.2.1)
|
16
16
|
activesupport (~> 5.2.1)
|
17
17
|
aws-sdk-apigateway
|
@@ -66,7 +66,7 @@ GEM
|
|
66
66
|
tzinfo (~> 1.1)
|
67
67
|
arel (9.0.0)
|
68
68
|
aws-eventstream (1.0.1)
|
69
|
-
aws-partitions (1.
|
69
|
+
aws-partitions (1.133.0)
|
70
70
|
aws-sdk-apigateway (1.23.0)
|
71
71
|
aws-sdk-core (~> 3, >= 3.39.0)
|
72
72
|
aws-sigv4 (~> 1.0)
|
@@ -87,7 +87,7 @@ GEM
|
|
87
87
|
aws-sdk-kms (1.13.0)
|
88
88
|
aws-sdk-core (~> 3, >= 3.39.0)
|
89
89
|
aws-sigv4 (~> 1.0)
|
90
|
-
aws-sdk-lambda (1.
|
90
|
+
aws-sdk-lambda (1.17.0)
|
91
91
|
aws-sdk-core (~> 3, >= 3.39.0)
|
92
92
|
aws-sigv4 (~> 1.0)
|
93
93
|
aws-sdk-s3 (1.30.1)
|
data/lib/jets/commands/new.rb
CHANGED
@@ -55,7 +55,7 @@ module Jets::Resource::ApiGateway::BasePath
|
|
55
55
|
# Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb
|
56
56
|
def rest_api_id
|
57
57
|
stack_name = Jets::Naming.parent_stack_name
|
58
|
-
return
|
58
|
+
return "RestApi" unless stack_exists?(stack_name)
|
59
59
|
|
60
60
|
stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
|
61
61
|
|
@@ -1,114 +1,12 @@
|
|
1
1
|
# Detects route changes
|
2
2
|
class Jets::Resource::ApiGateway::RestApi::Routes
|
3
|
-
class Change
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
if new_route && new_route.to != deployed_route.to
|
8
|
-
# change in already deployed route has been detected, requires bluegreen deploy
|
9
|
-
return true
|
10
|
-
end
|
11
|
-
end
|
12
|
-
false # Reaching here means no routes have been changed in a way that requires a bluegreen deploy
|
13
|
-
end
|
14
|
-
|
15
|
-
# Build up deployed routes from the existing CloudFormation resources.
|
16
|
-
def deployed_routes
|
17
|
-
routes = []
|
18
|
-
|
19
|
-
resources, position = [], true
|
20
|
-
while position
|
21
|
-
position = nil if position == true # start of loop
|
22
|
-
resp = apigateway.get_resources(
|
23
|
-
rest_api_id: rest_api_id,
|
24
|
-
position: position,
|
25
|
-
)
|
26
|
-
resources += resp.items
|
27
|
-
position = resp.position
|
28
|
-
end
|
29
|
-
|
30
|
-
resources.each do |resource|
|
31
|
-
resource_methods = resource.resource_methods
|
32
|
-
next if resource_methods.nil?
|
33
|
-
|
34
|
-
resource_methods.each do |http_verb, resource_method|
|
35
|
-
# puts "#{http_verb} #{resource.path} | resource.id #{resource.id}"
|
36
|
-
# puts to(resource.id, http_verb)
|
3
|
+
class Change
|
4
|
+
autoload :Base, 'jets/resource/api_gateway/rest_api/routes/change/base'
|
5
|
+
autoload :To, 'jets/resource/api_gateway/rest_api/routes/change/to'
|
6
|
+
autoload :Variable, 'jets/resource/api_gateway/rest_api/routes/change/variable'
|
37
7
|
|
38
|
-
|
39
|
-
|
40
|
-
next if http_verb == "OPTIONS"
|
41
|
-
|
42
|
-
path = recreate_path(resource.path)
|
43
|
-
method = http_verb.downcase.to_sym
|
44
|
-
to = to(resource.id, http_verb)
|
45
|
-
route = Jets::Route.new(path: path, method: method, to: to)
|
46
|
-
routes << route
|
47
|
-
end
|
48
|
-
end
|
49
|
-
routes
|
50
|
-
end
|
51
|
-
memoize :deployed_routes
|
52
|
-
|
53
|
-
# Find a route that has the same path and method. This is a comparable route
|
54
|
-
# Then we will compare the to or controller action to see if an already
|
55
|
-
# deployed route has been changed.
|
56
|
-
def find_comparable_route(deployed_route)
|
57
|
-
new_routes.find do |new_route|
|
58
|
-
new_route.path == deployed_route.path &&
|
59
|
-
new_route.method == deployed_route.method
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def recreate_path(path)
|
64
|
-
path = path.gsub(%r{^/},'')
|
65
|
-
path = path.sub(/{(.*)\+}/, '*\1')
|
66
|
-
path.sub(/{(.*)}/, ':\1')
|
67
|
-
end
|
68
|
-
|
69
|
-
def to(resource_id, http_method)
|
70
|
-
uri = method_uri(resource_id, http_method)
|
71
|
-
recreate_to(uri) unless uri.nil?
|
72
|
-
end
|
73
|
-
|
74
|
-
def method_uri(resource_id, http_method)
|
75
|
-
resp = apigateway.get_method(
|
76
|
-
rest_api_id: rest_api_id,
|
77
|
-
resource_id: resource_id,
|
78
|
-
http_method: http_method
|
79
|
-
)
|
80
|
-
resp.method_integration.uri
|
81
|
-
end
|
82
|
-
|
83
|
-
# Parses method uri and recreates a Route to argument.
|
84
|
-
# So:
|
85
|
-
# "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:112233445566:function:demo-test-posts_controller-new/invocations"
|
86
|
-
# Returns:
|
87
|
-
# posts#new
|
88
|
-
def recreate_to(method_uri)
|
89
|
-
md = method_uri.match(/function:(.*)\//)
|
90
|
-
function_name = md[1] # IE: demo-dev-posts_controller-new
|
91
|
-
controller_action = function_name.sub("#{Jets.project_namespace}-", '')
|
92
|
-
md = controller_action.match(/(.*)_controller-(.*)/)
|
93
|
-
controller = md[1]
|
94
|
-
controller = controller.gsub('-','/')
|
95
|
-
action = md[2]
|
96
|
-
"#{controller}##{action}" # IE: posts#new
|
97
|
-
end
|
98
|
-
|
99
|
-
# Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb
|
100
|
-
def rest_api_id
|
101
|
-
stack_name = Jets::Naming.parent_stack_name
|
102
|
-
return default unless stack_exists?(stack_name)
|
103
|
-
|
104
|
-
stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
|
105
|
-
|
106
|
-
api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
|
107
|
-
|
108
|
-
# resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources
|
109
|
-
stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
|
110
|
-
lookup(stack[:outputs], "RestApi") # rest_api_id
|
8
|
+
def changed?
|
9
|
+
To.changed? || Variable.changed? || ENV['JETS_REPLACE_API']
|
111
10
|
end
|
112
|
-
memoize :rest_api_id
|
113
11
|
end
|
114
12
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
class Jets::Resource::ApiGateway::RestApi::Routes::Change
|
2
|
+
class Base
|
3
|
+
extend Memoist
|
4
|
+
include Jets::AwsServices
|
5
|
+
|
6
|
+
# Build up deployed routes from the existing CloudFormation resources.
|
7
|
+
def deployed_routes
|
8
|
+
routes = []
|
9
|
+
|
10
|
+
resources, position = [], true
|
11
|
+
while position
|
12
|
+
position = nil if position == true # start of loop
|
13
|
+
resp = apigateway.get_resources(
|
14
|
+
rest_api_id: rest_api_id,
|
15
|
+
position: position,
|
16
|
+
)
|
17
|
+
resources += resp.items
|
18
|
+
position = resp.position
|
19
|
+
end
|
20
|
+
|
21
|
+
resources.each do |resource|
|
22
|
+
resource_methods = resource.resource_methods
|
23
|
+
next if resource_methods.nil?
|
24
|
+
|
25
|
+
resource_methods.each do |http_verb, resource_method|
|
26
|
+
# puts "#{http_verb} #{resource.path} | resource.id #{resource.id}"
|
27
|
+
# puts to(resource.id, http_verb)
|
28
|
+
|
29
|
+
# Test changing config.cors and CloudFormation does an in-place update
|
30
|
+
# on the resource. So no need to do bluegreen deployments for OPTIONS.
|
31
|
+
next if http_verb == "OPTIONS"
|
32
|
+
|
33
|
+
path = recreate_path(resource.path)
|
34
|
+
method = http_verb.downcase.to_sym
|
35
|
+
to = to(resource.id, http_verb)
|
36
|
+
route = Jets::Route.new(path: path, method: method, to: to)
|
37
|
+
routes << route
|
38
|
+
end
|
39
|
+
end
|
40
|
+
routes
|
41
|
+
end
|
42
|
+
memoize :deployed_routes
|
43
|
+
|
44
|
+
def recreate_path(path)
|
45
|
+
path = path.gsub(%r{^/},'')
|
46
|
+
path = path.sub(/{(.*)\+}/, '*\1')
|
47
|
+
path.sub(/{(.*)}/, ':\1')
|
48
|
+
end
|
49
|
+
|
50
|
+
def to(resource_id, http_method)
|
51
|
+
uri = method_uri(resource_id, http_method)
|
52
|
+
recreate_to(uri) unless uri.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_uri(resource_id, http_method)
|
56
|
+
resp = apigateway.get_method(
|
57
|
+
rest_api_id: rest_api_id,
|
58
|
+
resource_id: resource_id,
|
59
|
+
http_method: http_method
|
60
|
+
)
|
61
|
+
resp.method_integration.uri
|
62
|
+
end
|
63
|
+
|
64
|
+
# Parses method uri and recreates a Route to argument.
|
65
|
+
# So:
|
66
|
+
# "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:112233445566:function:demo-test-posts_controller-new/invocations"
|
67
|
+
# Returns:
|
68
|
+
# posts#new
|
69
|
+
def recreate_to(method_uri)
|
70
|
+
md = method_uri.match(/function:(.*)\//)
|
71
|
+
function_name = md[1] # IE: demo-dev-posts_controller-new
|
72
|
+
controller_action = function_name.sub("#{Jets.project_namespace}-", '')
|
73
|
+
md = controller_action.match(/(.*)_controller-(.*)/)
|
74
|
+
controller = md[1]
|
75
|
+
controller = controller.gsub('-','/')
|
76
|
+
action = md[2]
|
77
|
+
"#{controller}##{action}" # IE: posts#new
|
78
|
+
end
|
79
|
+
|
80
|
+
# Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb
|
81
|
+
def rest_api_id
|
82
|
+
stack_name = Jets::Naming.parent_stack_name
|
83
|
+
return "RestApi" unless stack_exists?(stack_name)
|
84
|
+
|
85
|
+
stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
|
86
|
+
|
87
|
+
api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
|
88
|
+
|
89
|
+
# resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources
|
90
|
+
stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
|
91
|
+
lookup(stack[:outputs], "RestApi") # rest_api_id
|
92
|
+
end
|
93
|
+
memoize :rest_api_id
|
94
|
+
|
95
|
+
def new_routes
|
96
|
+
Jets::Router.routes
|
97
|
+
end
|
98
|
+
memoize :new_routes
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Detects route to changes
|
2
|
+
class Jets::Resource::ApiGateway::RestApi::Routes::Change
|
3
|
+
class To < Base
|
4
|
+
def self.changed?
|
5
|
+
new.changed?
|
6
|
+
end
|
7
|
+
|
8
|
+
def changed?
|
9
|
+
deployed_routes.each do |deployed_route|
|
10
|
+
new_route = find_comparable_route(deployed_route)
|
11
|
+
if new_route && new_route.to != deployed_route.to
|
12
|
+
# change in already deployed route has been detected, requires bluegreen deploy
|
13
|
+
return true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
false # Reaching here means no routes have been changed in a way that requires a bluegreen deploy
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find a route that has the same path and method. This is a comparable route
|
20
|
+
# Then we will compare the to or controller action to see if an already
|
21
|
+
# deployed route has been changed.
|
22
|
+
def find_comparable_route(deployed_route)
|
23
|
+
new_routes.find do |new_route|
|
24
|
+
new_route.path == deployed_route.path &&
|
25
|
+
new_route.method == deployed_route.method
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Detects route variable changes
|
2
|
+
class Jets::Resource::ApiGateway::RestApi::Routes::Change
|
3
|
+
class Variable < Base
|
4
|
+
def self.changed?
|
5
|
+
new.changed?
|
6
|
+
end
|
7
|
+
|
8
|
+
def changed?
|
9
|
+
changed = false
|
10
|
+
deployed_routes.each do |deployed_route|
|
11
|
+
parent = collision.variable_parent(deployed_route.path)
|
12
|
+
parent_variables = collision.parent_variables(parent, [deployed_route.path])
|
13
|
+
new_parent_variables = collision.parent_variables(parent, new_paths)
|
14
|
+
|
15
|
+
changed = parent_variables.size > 0 && new_parent_variables.size > 0 &&
|
16
|
+
parent_variables != new_parent_variables
|
17
|
+
break if changed
|
18
|
+
end
|
19
|
+
changed
|
20
|
+
end
|
21
|
+
|
22
|
+
# Only consider paths with variables
|
23
|
+
def new_paths
|
24
|
+
new_routes.map(&:path).select {|p| p.include?(':')}.uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
# Only consider deployed routes with variables
|
28
|
+
def deployed_routes
|
29
|
+
deployed_routes = super
|
30
|
+
deployed_routes.select do |route|
|
31
|
+
route.path.include?(':')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def collision
|
36
|
+
@collision ||= Jets::Resource::ApiGateway::RestApi::Routes::Collision.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
# Detects path variable collisions
|
2
2
|
class Jets::Resource::ApiGateway::RestApi::Routes
|
3
|
-
class Collision
|
3
|
+
class Collision
|
4
4
|
autoload :VariableException, 'jets/resource/api_gateway/rest_api/routes/collision/variable_exception'
|
5
5
|
|
6
6
|
attr_reader :collisions
|
7
|
-
def initialize
|
8
|
-
@routes = routes
|
7
|
+
def initialize
|
9
8
|
@collisions = []
|
10
9
|
end
|
11
10
|
|
12
|
-
def collision?
|
13
|
-
paths = paths_with_variables(
|
11
|
+
def collision?(paths)
|
12
|
+
paths = paths_with_variables(paths)
|
14
13
|
parents = variable_parents(paths)
|
15
14
|
|
16
15
|
collide = false
|
@@ -21,7 +20,11 @@ class Jets::Resource::ApiGateway::RestApi::Routes
|
|
21
20
|
end
|
22
21
|
|
23
22
|
def exception
|
24
|
-
collision_message
|
23
|
+
VariableException.new(collision_message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def collision_message
|
27
|
+
<<~EOL
|
25
28
|
There are routes with sibling variables under the same parent that collide.
|
26
29
|
|
27
30
|
Collisions:
|
@@ -34,7 +37,6 @@ class Jets::Resource::ApiGateway::RestApi::Routes
|
|
34
37
|
Please check your `config/routes.rb` and remove the colliding routes.
|
35
38
|
More info: http://rubyonjets.com/docs/considerations-api-gateway/
|
36
39
|
EOL
|
37
|
-
VariableException.new(collision_message)
|
38
40
|
end
|
39
41
|
|
40
42
|
def variable_collision_exists?(parent, paths)
|
@@ -88,13 +90,9 @@ class Jets::Resource::ApiGateway::RestApi::Routes
|
|
88
90
|
parents.uniq.sort
|
89
91
|
end
|
90
92
|
|
91
|
-
def paths_with_variables(paths)
|
92
|
-
paths.select { |p| p.include?(':') }.uniq
|
93
|
-
end
|
94
|
-
|
95
93
|
# Strips the path down until only the leaf node part is a variable
|
96
94
|
# Example: users/:user_id/posts/:post_id/edit
|
97
|
-
# Returns: users/:user_id/posts
|
95
|
+
# Returns: users/:user_id/posts
|
98
96
|
def variable_parent(path)
|
99
97
|
path = variable_leaf(path)
|
100
98
|
# drop last variable to leave behind the parent
|
@@ -105,7 +103,7 @@ class Jets::Resource::ApiGateway::RestApi::Routes
|
|
105
103
|
# Example: users/:user_id/posts/:post_id/edit
|
106
104
|
# Returns: users/:user_id/posts
|
107
105
|
def variable_leaf(path)
|
108
|
-
return unless path.include?(':')
|
106
|
+
return '' unless path.include?(':')
|
109
107
|
|
110
108
|
parts = path.split('/')
|
111
109
|
is_variable = parts.last.include?(':')
|
@@ -115,5 +113,9 @@ class Jets::Resource::ApiGateway::RestApi::Routes
|
|
115
113
|
end
|
116
114
|
parts[0..-1].join('/') # parent
|
117
115
|
end
|
116
|
+
|
117
|
+
def paths_with_variables(paths)
|
118
|
+
paths.select { |p| p.include?(':') }.uniq
|
119
|
+
end
|
118
120
|
end
|
119
121
|
end
|
data/lib/jets/router.rb
CHANGED
@@ -16,8 +16,9 @@ module Jets
|
|
16
16
|
|
17
17
|
# Validate routes that deployable
|
18
18
|
def check_collision!
|
19
|
-
|
20
|
-
|
19
|
+
paths = self.routes.map(&:path)
|
20
|
+
collision = Jets::Resource::ApiGateway::RestApi::Routes::Collision.new
|
21
|
+
collide = collision.collision?(paths)
|
21
22
|
raise collision.exception if collide
|
22
23
|
end
|
23
24
|
|
data/lib/jets/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jets
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tung Nguyen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -704,8 +704,10 @@ files:
|
|
704
704
|
- lib/jets/resource/api_gateway/rest_api/change_detection.rb
|
705
705
|
- lib/jets/resource/api_gateway/rest_api/logical_id.rb
|
706
706
|
- lib/jets/resource/api_gateway/rest_api/routes.rb
|
707
|
-
- lib/jets/resource/api_gateway/rest_api/routes/base.rb
|
708
707
|
- lib/jets/resource/api_gateway/rest_api/routes/change.rb
|
708
|
+
- lib/jets/resource/api_gateway/rest_api/routes/change/base.rb
|
709
|
+
- lib/jets/resource/api_gateway/rest_api/routes/change/to.rb
|
710
|
+
- lib/jets/resource/api_gateway/rest_api/routes/change/variable.rb
|
709
711
|
- lib/jets/resource/api_gateway/rest_api/routes/collision.rb
|
710
712
|
- lib/jets/resource/api_gateway/rest_api/routes/collision/variable_exception.rb
|
711
713
|
- lib/jets/resource/associated.rb
|