aws-sdk-code-generator 0.2.2.pre → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-sdk-code-generator/api.rb +12 -2
  3. data/lib/aws-sdk-code-generator/client_constructor.rb +4 -1
  4. data/lib/aws-sdk-code-generator/client_operation_documentation.rb +27 -7
  5. data/lib/aws-sdk-code-generator/client_response_structure_example.rb +8 -1
  6. data/lib/aws-sdk-code-generator/code_builder.rb +78 -9
  7. data/lib/aws-sdk-code-generator/codegenerated_plugin.rb +34 -0
  8. data/lib/aws-sdk-code-generator/gem_builder.rb +27 -5
  9. data/lib/aws-sdk-code-generator/helper.rb +10 -1
  10. data/lib/aws-sdk-code-generator/plugin_list.rb +30 -9
  11. data/lib/aws-sdk-code-generator/service.rb +22 -0
  12. data/lib/aws-sdk-code-generator/views/async_client_class.rb +6 -2
  13. data/lib/aws-sdk-code-generator/views/client_api_module.rb +40 -5
  14. data/lib/aws-sdk-code-generator/views/client_class.rb +7 -2
  15. data/lib/aws-sdk-code-generator/views/endpoint_parameters_class.rb +80 -0
  16. data/lib/aws-sdk-code-generator/views/endpoint_provider_class.rb +34 -0
  17. data/lib/aws-sdk-code-generator/views/endpoints_module.rb +177 -0
  18. data/lib/aws-sdk-code-generator/views/endpoints_plugin.rb +85 -0
  19. data/lib/aws-sdk-code-generator/views/gemspec.rb +6 -2
  20. data/lib/aws-sdk-code-generator/views/service_module.rb +22 -0
  21. data/lib/aws-sdk-code-generator/views/spec/endpoint_provider_spec_class.rb +198 -0
  22. data/lib/aws-sdk-code-generator/views/spec/spec_helper.rb +9 -4
  23. data/lib/aws-sdk-code-generator/views/types_module.rb +60 -24
  24. data/lib/aws-sdk-code-generator.rb +7 -1
  25. data/templates/async_client_class.mustache +5 -7
  26. data/templates/client_api_module.mustache +7 -0
  27. data/templates/client_class.mustache +4 -1
  28. data/templates/endpoint_parameters_class.mustache +50 -0
  29. data/templates/endpoint_provider_class.mustache +30 -0
  30. data/templates/endpoints_module.mustache +33 -0
  31. data/templates/endpoints_plugin.mustache +76 -0
  32. data/templates/gemspec.mustache +2 -1
  33. data/templates/license.txt +202 -0
  34. data/templates/service_module.mustache +11 -1
  35. data/templates/spec/endpoint_provider_spec_class.mustache +76 -0
  36. data/templates/spec/spec_helper.mustache +5 -0
  37. data/templates/types_module.mustache +7 -0
  38. metadata +16 -4
@@ -30,7 +30,10 @@ module AwsSdkCodeGenerator
30
30
  'timestampFormat' => true, # glacier api customization
31
31
  'xmlNamespace' => true,
32
32
  'streaming' => true, # transfer-encoding
33
- 'requiresLength' => true, # transder-encoding
33
+ 'requiresLength' => true, # transfer-encoding
34
+ 'union' => false, # should remain false
35
+ 'document' => true,
36
+ 'jsonvalue' => true,
34
37
  # event stream modeling
35
38
  'event' => false,
36
39
  'eventstream' => false,
@@ -60,7 +63,7 @@ module AwsSdkCodeGenerator
60
63
  'max' => false,
61
64
  'wrapper' => false,
62
65
  'xmlOrder' => false,
63
- 'retryable' => false
66
+ 'retryable' => false,
64
67
  }
65
68
 
66
69
  METADATA_KEYS = {
@@ -81,7 +84,10 @@ module AwsSdkCodeGenerator
81
84
  'checksumFormat' => true,
82
85
  'globalEndpoint' => true,
83
86
  'serviceAbbreviation' => true,
84
- 'uid' => true
87
+ 'uid' => true,
88
+ 'awsQueryCompatible' => true, # AwsQuery migration
89
+ # ignore
90
+ 'ripServiceName' => true
85
91
  }
86
92
 
87
93
  # @option options [required, Service] :service
@@ -127,13 +133,22 @@ module AwsSdkCodeGenerator
127
133
  shape_name = lstrip_prefix(upcase_first(shape_name))
128
134
  end
129
135
  lines = []
130
- if non_error_struct?(shape)
136
+ if non_error_struct?(shape) && !document_struct?(shape)
131
137
  required = Set.new(shape['required'] || [])
132
138
  unless shape['members'].nil?
133
139
  shape['members'].each do |member_name, member_ref|
134
140
  lines << "#{shape_name}.add_member(:#{underscore(member_name)}, #{shape_ref(member_ref, member_name, required)})"
135
141
  end
136
142
  end
143
+ if shape['union']
144
+ lines << "#{shape_name}.add_member(:unknown, Shapes::ShapeRef.new(shape: nil, location_name: 'unknown'))"
145
+ shape['members'].each do |member_name, member_ref|
146
+ member_name_underscore = underscore(member_name)
147
+ member_class_name = pascal_case(member_name_underscore)
148
+ lines << "#{shape_name}.add_member_subclass(:#{member_name_underscore}, Types::#{shape_name}::#{member_class_name})"
149
+ end
150
+ lines << "#{shape_name}.add_member_subclass(:unknown, Types::#{shape_name}::Unknown)"
151
+ end
137
152
  lines << "#{shape_name}.struct_class = Types::#{shape_name}"
138
153
  if payload = shape['payload']
139
154
  lines << "#{shape_name}[:payload] = :#{underscore(payload)}"
@@ -186,6 +201,15 @@ module AwsSdkCodeGenerator
186
201
  o.http_method = operation['http']['method']
187
202
  o.http_request_uri = operation['http']['requestUri']
188
203
  o.http_checksum_required = true if operation['httpChecksumRequired']
204
+ if operation.key?('httpChecksum')
205
+ operation['httpChecksum']['requestAlgorithmMember'] = underscore(operation['httpChecksum']['requestAlgorithmMember']) if operation['httpChecksum']['requestAlgorithmMember']
206
+ operation['httpChecksum']['requestValidationModeMember'] = underscore(operation['httpChecksum']['requestValidationModeMember']) if operation['httpChecksum']['requestValidationModeMember']
207
+
208
+ o.http_checksum = operation['httpChecksum'].inject([]) do |a, (k, v)|
209
+ a << { key: k.inspect, value: v.inspect }
210
+ a
211
+ end
212
+ end
189
213
  %w(input output).each do |key|
190
214
  if operation[key]
191
215
  o.shape_references << "o.#{key} = #{operation_ref(operation[key])}"
@@ -258,7 +282,11 @@ module AwsSdkCodeGenerator
258
282
  if @service.protocol == 'api-gateway' && type == 'timestamp'
259
283
  shape['timestampFormat'] = 'iso8601'
260
284
  end
261
- if SHAPE_CLASSES.key?(type)
285
+ if document_struct?(shape)
286
+ ["Shapes::DocumentShape", shape]
287
+ elsif shape['union']
288
+ ["Shapes::UnionShape", shape]
289
+ elsif SHAPE_CLASSES.key?(type)
262
290
  ["Shapes::#{SHAPE_CLASSES[type]}", shape]
263
291
  else
264
292
  raise ArgumentError, "unsupported shape type `#{type}'"
@@ -315,6 +343,10 @@ module AwsSdkCodeGenerator
315
343
  (shape['error'] || shape['exception'])
316
344
  end
317
345
 
346
+ def document_struct?(shape)
347
+ shape['type'] == 'structure' && shape['document']
348
+ end
349
+
318
350
  def structure_shape_enum
319
351
  Enumerator.new do |y|
320
352
  shape_enum.each do |shape_name, shape|
@@ -516,6 +548,9 @@ module AwsSdkCodeGenerator
516
548
  # @return [Boolean]
517
549
  attr_accessor :http_checksum_required
518
550
 
551
+ # @return [Hash]
552
+ attr_accessor :http_checksum
553
+
519
554
  # @return [Array<String>]
520
555
  attr_accessor :shape_references
521
556
 
@@ -18,6 +18,7 @@ module AwsSdkCodeGenerator
18
18
  # @option options [required, Hash] :api
19
19
  # @option options [Hash] :waiters
20
20
  # @option options [Hash] :client_examples
21
+ # @option options [Array<CodegeneratedPlugin] :codegenerated_plugins
21
22
  def initialize(options)
22
23
  @service_identifier = options.fetch(:service_identifier)
23
24
  @service_name = options.fetch(:service_name)
@@ -25,7 +26,11 @@ module AwsSdkCodeGenerator
25
26
  @gem_name = options.fetch(:gem_name)
26
27
  @gem_version = options.fetch(:gem_version)
27
28
  @plugins = PluginList.new(options)
28
- @client_constructor = ClientConstructor.new(options.merge(plugins: @plugins))
29
+ @codegenerated_plugins = options.fetch(:codegenerated_plugins, [])
30
+ @client_constructor = ClientConstructor.new(
31
+ options.merge(
32
+ plugins: @plugins,
33
+ codegenerated_plugins: @codegenerated_plugins))
29
34
  @operations = ClientOperationList.new(options).to_a
30
35
  @waiters = Waiter.build_list(options[:waiters])
31
36
  @custom = options.fetch(:custom)
@@ -65,7 +70,7 @@ module AwsSdkCodeGenerator
65
70
 
66
71
  # @return [Array<String>]
67
72
  def plugin_class_names
68
- @plugins.map(&:class_name)
73
+ @plugins.map(&:class_name) + @codegenerated_plugins.map(&:class_name)
69
74
  end
70
75
 
71
76
  # @return [Array<Waiter>]
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsSdkCodeGenerator
4
+ module Views
5
+ class EndpointParametersClass < View
6
+
7
+ # @option options [required, Service] :service
8
+ def initialize(options)
9
+ @service = options.fetch(:service)
10
+ if (parameters = @service.endpoint_rules&.fetch('parameters'))
11
+ @parameters = parameters.map do |k,p|
12
+ EndpointParameter.new(k, p)
13
+ end
14
+ end
15
+ end
16
+
17
+ # @return [Array<EndpointParameter>]
18
+ attr_reader :parameters
19
+
20
+ # @return [String|nil]
21
+ def generated_src_warning
22
+ return if @service.protocol == 'api-gateway'
23
+ GENERATED_SRC_WARNING
24
+ end
25
+
26
+ def module_name
27
+ @service.module_name
28
+ end
29
+
30
+ class EndpointParameter
31
+ def initialize(name, definition={})
32
+ @name = name
33
+ @type = definition['type']
34
+ @built_in = definition['builtIn']
35
+ @default = definition['default']
36
+ @required = definition['required']
37
+ @documentation = "# @!attribute #{underscore_name}\n"
38
+ if definition['documentation']
39
+ @documentation += " # #{definition['documentation']}\n"
40
+ end
41
+ if deprecated = definition['deprecated']
42
+ @documentation += " #\n # @deprecated\n"
43
+ if deprecated['message']
44
+ @documentation += " # #{deprecated['message']}\n"
45
+ end
46
+ if deprecated['since']
47
+ @documentation += " # Since: #{deprecated['since']}\n"
48
+ end
49
+ end
50
+ @documentation += " #\n # @return [#{@type}]\n #"
51
+ end
52
+
53
+ # @return [String]
54
+ attr_reader :name
55
+
56
+ # @return [String]
57
+ attr_reader :documentation
58
+
59
+ # @return [Boolean]
60
+ attr_reader :required
61
+
62
+ # @return [String,Boolean]
63
+ attr_reader :default
64
+
65
+ def default?
66
+ !@default.nil?
67
+ end
68
+
69
+ def boolean_default?
70
+ default? && (@default == true || @default == false)
71
+ end
72
+
73
+ def underscore_name
74
+ Underscore.underscore(name)
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsSdkCodeGenerator
4
+ module Views
5
+ class EndpointProviderClass < View
6
+
7
+ # @option options [required, Service] :service
8
+ # @option options [required, Hash] :endpoint_rules
9
+ def initialize(options)
10
+ @service = options.fetch(:service)
11
+ @endpoint_rules = @service.endpoint_rules
12
+
13
+ version = @endpoint_rules['version']
14
+ return if version&.match(/^\d+\.\d+$/) # && version == '1.0'
15
+
16
+ raise 'Endpoint Rules version must be 1.0'
17
+ end
18
+
19
+ # @return [String|nil]
20
+ def generated_src_warning
21
+ return if @service.protocol == 'api-gateway'
22
+ GENERATED_SRC_WARNING
23
+ end
24
+
25
+ def module_name
26
+ @service.module_name
27
+ end
28
+
29
+ def endpoint_rules_encoded
30
+ Base64.encode64(JSON.dump(@endpoint_rules))
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsSdkCodeGenerator
4
+ module Views
5
+ class EndpointsModule < View
6
+ # @option options [required, Service] :service
7
+ def initialize(options)
8
+ @service = options.fetch(:service)
9
+
10
+ @parameters = @service.endpoint_rules.fetch('parameters', {})
11
+
12
+ @endpoint_classes = @service.api['operations'].each.with_object([]) do
13
+ |(name, op), array|
14
+ array << EndpointClass.new(
15
+ name: name,
16
+ parameters: endpoint_parameters_for_operation(op)
17
+ )
18
+ end
19
+ end
20
+
21
+ # @return [Array<EndpointClass>]
22
+ attr_reader :endpoint_classes
23
+
24
+ # @return [String|nil]
25
+ def generated_src_warning
26
+ return if @service.protocol == 'api-gateway'
27
+ GENERATED_SRC_WARNING
28
+ end
29
+
30
+ def module_name
31
+ @service.module_name
32
+ end
33
+
34
+ class EndpointClass
35
+ def initialize(options)
36
+ @name = options[:name]
37
+ @parameters = options[:parameters]
38
+ end
39
+
40
+ # @return [String]
41
+ attr_reader :name
42
+
43
+ # @return [Array<EndpointParameter>]
44
+ attr_reader :parameters
45
+
46
+ def has_endpoint_built_in?
47
+ parameters.any? { |p| p.param_data['builtIn'] == 'SDK::Endpoint' }
48
+ end
49
+ end
50
+
51
+ class EndpointParameter
52
+ def initialize(options)
53
+ @key = options[:key]
54
+ @value = options[:value]
55
+ @source = options[:source]
56
+ @param_data = options[:param_data]
57
+ end
58
+
59
+ # @return [String]
60
+ attr_accessor :key
61
+
62
+ # @return [String]
63
+ attr_accessor :value
64
+
65
+ # @return [String]
66
+ attr_accessor :source
67
+
68
+ # @return [Hash]
69
+ attr_accessor :param_data
70
+
71
+ def static_string?
72
+ @source == 'staticContextParam' && value.is_a?(String)
73
+ end
74
+ end
75
+
76
+
77
+ private
78
+
79
+ def endpoint_parameters_for_operation(operation)
80
+ @parameters.each.with_object([]) do |(param_name, param_data), endpoint_parameters|
81
+ value, source = endpoint_parameter_value(
82
+ operation, param_name, param_data
83
+ )
84
+
85
+ endpoint_parameters << EndpointParameter.new(
86
+ key: Underscore.underscore(param_name),
87
+ value: value,
88
+ source: source,
89
+ param_data: param_data
90
+ )
91
+ end
92
+ end
93
+
94
+ # Most to least
95
+ # staticContextParams
96
+ # contextParam
97
+ # clientContextParams
98
+ # Built-In Bindings
99
+ # Built-in binding default values
100
+ def endpoint_parameter_value(operation, param_name, param_data)
101
+ value, source = [
102
+ static_context_param(operation, param_name), 'staticContextParam'
103
+ ]
104
+ value, source = [
105
+ context_param_value(operation, param_name), 'contextParam'
106
+ ] unless value
107
+ value, source = [
108
+ client_context_param_value(param_name, param_data),
109
+ 'clientContextParam'
110
+ ] unless value
111
+ value, source = [
112
+ built_in_client_context_param_value(param_data), 'builtIn'
113
+ ] unless value
114
+
115
+ [value || 'nil', source]
116
+ end
117
+
118
+ def client_context_param_value(param_name, param_data)
119
+ if @service.api['clientContextParams']&.key?(param_name) &&
120
+ !param_data['builtIn']
121
+ "context.config.#{Underscore.underscore(param_name)}"
122
+ end
123
+ end
124
+
125
+ def built_in_client_context_param_value(param_data)
126
+ case param_data['builtIn']
127
+ when 'AWS::Region'
128
+ 'context.config.region'
129
+ when 'AWS::UseFIPS'
130
+ 'context.config.use_fips_endpoint'
131
+ when 'AWS::UseDualStack'
132
+ if @service.name == 'S3' || @service.name == 'S3Control'
133
+ 'context[:use_dualstack_endpoint]'
134
+ else
135
+ 'context.config.use_dualstack_endpoint'
136
+ end
137
+ when 'AWS::STS::UseGlobalEndpoint'
138
+ "context.config.sts_regional_endpoints == 'legacy'"
139
+ when 'AWS::S3::UseGlobalEndpoint'
140
+ "context.config.s3_us_east_1_regional_endpoint == 'legacy'"
141
+ when 'AWS::S3::Accelerate'
142
+ if @service.name == 'S3' || @service.name == 'S3Control'
143
+ 'context[:use_accelerate_endpoint]'
144
+ else
145
+ 'context.config.use_accelerate_endpoint'
146
+ end
147
+ when 'AWS::S3::ForcePathStyle'
148
+ 'context.config.force_path_style'
149
+ when 'AWS::S3::UseArnRegion', 'AWS::S3Control::UseArnRegion'
150
+ 'context.config.s3_use_arn_region'
151
+ when 'AWS::S3::DisableMultiRegionAccessPoints'
152
+ 'context.config.s3_disable_multiregion_access_points'
153
+ when 'SDK::Endpoint'
154
+ 'endpoint'
155
+ end
156
+ end
157
+
158
+ def context_param_value(operation, param_name)
159
+ return nil unless operation['input']
160
+
161
+ input_shape = operation['input']['shape']
162
+ members = @service.api['shapes'][input_shape].fetch('members', {})
163
+ members.detect do |(member_name, member)|
164
+ context_param = member.fetch('contextParam', {})
165
+ if context_param.fetch('name', nil) == param_name
166
+ break "context.params[:#{Underscore.underscore(member_name)}]"
167
+ end
168
+ end
169
+ end
170
+
171
+ def static_context_param(operation, param_name)
172
+ operation.fetch('staticContextParams', {})
173
+ .fetch(param_name, {}).fetch('value', nil)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsSdkCodeGenerator
4
+ module Views
5
+ class EndpointsPlugin < View
6
+ # @option options [required, Service] :service
7
+ def initialize(options)
8
+ @service = options.fetch(:service)
9
+ if (client_options = @service.api['clientContextParams'])
10
+ endpoint_parameters = @service.endpoint_rules.fetch('parameters', {})
11
+
12
+ @endpoint_options = client_options.each.with_object([]) do |(name, _data), array|
13
+ param_data = endpoint_parameters[name]
14
+
15
+ next if param_data['builtIn']
16
+
17
+ array << EndpointOption.new(
18
+ name: Underscore.underscore(name),
19
+ docstring: param_data['documentation'],
20
+ doc_type: param_data['type'],
21
+ default: param_data['default']
22
+ )
23
+ end
24
+ end
25
+ @endpoint_classes = @service.api['operations'].each.with_object([]) do
26
+ |(op, _api), array|
27
+ array << EndpointClass.new(
28
+ operation_name: Underscore.underscore(op),
29
+ class_name: op
30
+ )
31
+ end
32
+ end
33
+
34
+ # @return [Array<EndpointClass>]
35
+ attr_reader :endpoint_classes
36
+
37
+ # @return [Array<EndpointOption>, nil]
38
+ attr_reader :endpoint_options
39
+
40
+ # @return [String|nil]
41
+ def generated_src_warning
42
+ return if @service.protocol == 'api-gateway'
43
+ GENERATED_SRC_WARNING
44
+ end
45
+
46
+ def module_name
47
+ @service.module_name
48
+ end
49
+
50
+ class EndpointClass
51
+ def initialize(options)
52
+ @operation_name = options[:operation_name]
53
+ @class_name = options[:class_name]
54
+ end
55
+
56
+ # @return [String]
57
+ attr_reader :operation_name
58
+
59
+ # @return [String]
60
+ attr_reader :class_name
61
+ end
62
+
63
+ class EndpointOption
64
+ def initialize(options)
65
+ @name = options[:name]
66
+ @doc_type = options[:doc_type]
67
+ @default = options[:default].nil? ? 'nil' : options[:default]
68
+ @docstring = options[:docstring]
69
+ end
70
+
71
+ # @return [String]
72
+ attr_reader :name
73
+
74
+ # @return [String]
75
+ attr_reader :doc_type
76
+
77
+ # @return [Boolean,String]
78
+ attr_reader :default
79
+
80
+ # @return [String]
81
+ attr_reader :docstring
82
+ end
83
+ end
84
+ end
85
+ end
@@ -30,7 +30,7 @@ module AwsSdkCodeGenerator
30
30
  end
31
31
 
32
32
  def email
33
- @custom ? 'yourname@email.com' : 'trevrowe@amazon.com'
33
+ @custom ? 'yourname@email.com' : 'aws-dr-rubygems@amazon.com'
34
34
  end
35
35
 
36
36
  # @return [String]
@@ -42,6 +42,10 @@ module AwsSdkCodeGenerator
42
42
  @custom ? false : true
43
43
  end
44
44
 
45
+ def files
46
+ ['LICENSE.txt', 'CHANGELOG.md', 'VERSION', 'lib/**/*.rb']
47
+ end
48
+
45
49
  # @return [String]
46
50
  def description
47
51
  if @custom
@@ -58,7 +62,7 @@ module AwsSdkCodeGenerator
58
62
 
59
63
  # @return [String]
60
64
  def code_uri
61
- "https://github.com/aws/aws-sdk-ruby/tree/master/gems/#{gem_name}"
65
+ "https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/#{gem_name}"
62
66
  end
63
67
 
64
68
  # @return [Array<Dependency>]
@@ -11,6 +11,7 @@ module AwsSdkCodeGenerator
11
11
  def initialize(options)
12
12
  @service = options.fetch(:service)
13
13
  @prefix = options.fetch(:prefix)
14
+ @codegenerated_plugins = options.fetch(:codegenerated_plugins)
14
15
  end
15
16
 
16
17
  # @return [String|nil]
@@ -51,15 +52,36 @@ module AwsSdkCodeGenerator
51
52
  @service.gem_dependencies.keys
52
53
  end
53
54
 
55
+ # @return [Boolean] - Return true if a check is needed before
56
+ # requiring core to prevent circular dependencies.
57
+ # This is required to support backwards compatibility for SSO which was
58
+ # moved from the aws-sdk-sso gem into aws-sdk-core.
59
+ def require_core_guard?
60
+ @service.included_in_core?
61
+ end
62
+
54
63
  # @return [Array<String>]
55
64
  def relative_requires
56
65
  paths = Set.new
57
66
  paths << "#{@prefix}/types"
58
67
  paths << "#{@prefix}/client_api"
68
+
69
+ # these must be required before the client
70
+ if @codegenerated_plugins
71
+ paths += @codegenerated_plugins.map { | p| p.path }
72
+ end
73
+
59
74
  paths << "#{@prefix}/client"
60
75
  paths << "#{@prefix}/errors"
61
76
  paths << "#{@prefix}/waiters" if @service.waiters
62
77
  paths << "#{@prefix}/resource"
78
+
79
+ unless @service.legacy_endpoints?
80
+ paths << "#{@prefix}/endpoint_parameters"
81
+ paths << "#{@prefix}/endpoint_provider"
82
+ paths << "#{@prefix}/endpoints"
83
+ end
84
+
63
85
  if @service.resources && @service.resources['resources']
64
86
  @service.resources['resources'].keys.each do |resource_name|
65
87
  path = "#{@prefix}/#{underscore(resource_name)}"