google-apis-generator 0.1.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.
@@ -0,0 +1,99 @@
1
+ # Copyright 2015 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'google/apis/discovery_v1'
16
+ require 'google/apis/generator/annotator'
17
+ require 'google/apis/generator/model'
18
+ require 'google/apis/generator/template'
19
+ require 'google/apis/generator/updater'
20
+ require 'google/apis/generator/version'
21
+ require 'active_support'
22
+ require 'active_support/core_ext'
23
+ require 'active_support/inflector'
24
+ require 'yaml'
25
+
26
+ module Google
27
+ module Apis
28
+ # Generates ruby classes for APIs from discovery documents
29
+ # @private
30
+ class Generator
31
+ Discovery = Google::Apis::DiscoveryV1
32
+
33
+ Result = Struct.new :files, :version_path, :changelog_path, :gem_name, :revision
34
+
35
+ # Load templates
36
+ def initialize(api_names: nil, api_names_out: nil)
37
+ @names = Google::Apis::Generator::Names.new(api_names_out || File.join(Google::Apis::ROOT, "api_names_out.yaml"),
38
+ api_names || File.join(Google::Apis::ROOT, "api_names.yaml"))
39
+ @templates = {}
40
+ end
41
+
42
+ # Generates ruby source for an API
43
+ #
44
+ # @param [String] json
45
+ # API Description, as JSON text
46
+ # @return [Hash<String,String>]
47
+ # Hash of generated files keyed by path
48
+ def render(json)
49
+ api = parse_description(json)
50
+ Annotator.process(api, @names)
51
+ context = {
52
+ "api" => api,
53
+ "generator_version" => Google::Apis::Generator::VERSION
54
+ }
55
+
56
+ base_path = api.gem_name
57
+ lib_path = File.join(base_path, "lib")
58
+ spec_path = File.join(base_path, "spec")
59
+ module_path = File.join(lib_path, ActiveSupport::Inflector.underscore(api.qualified_name))
60
+
61
+ result = Result.new({},
62
+ File.join(module_path, "gem_version.rb"),
63
+ File.join(base_path, "CHANGELOG.md"),
64
+ api.gem_name,
65
+ api.revision)
66
+ result.files[File.join(base_path, ".rspec")] = render_template("dot-rspec", context)
67
+ result.files[File.join(base_path, ".yardopts")] = render_template("dot-yardopts", context)
68
+ result.files[result.changelog_path] = render_template("initial-changelog.md", context)
69
+ result.files[File.join(base_path, "Gemfile")] = render_template("gemfile", context)
70
+ result.files[File.join(base_path, "#{api.gem_name}.gemspec")] = render_template("gemspec", context)
71
+ result.files[File.join(base_path, "LICENSE.md")] = render_template("license.md", context)
72
+ result.files[File.join(base_path, "OVERVIEW.md")] = render_template("overview.md", context)
73
+ result.files[File.join(base_path, "Rakefile")] = render_template("rakefile", context)
74
+ result.files[File.join(lib_path, "#{api.gem_name}.rb")] = render_template("entry-point.rb", context)
75
+ result.files[module_path + ".rb"] = render_template("module.rb", context)
76
+ result.files[File.join(module_path, "classes.rb")] = render_template("classes.rb", context)
77
+ result.files[File.join(module_path, "representations.rb")] = render_template("representations.rb", context)
78
+ result.files[File.join(module_path, "service.rb")] = render_template("service.rb", context)
79
+ result.files[result.version_path] = render_template("initial-gem_version.rb", context)
80
+ result.files[File.join(spec_path, "generated_spec.rb")] = render_template("generated_spec.rb", context)
81
+ result
82
+ end
83
+
84
+ def render_template(name, context)
85
+ (@templates[name] ||= Template.load(name)).render(context)
86
+ end
87
+
88
+ # Dump mapping of API names
89
+ # @return [String] Mapping of paths to ruby names in YAML format
90
+ def dump_api_names
91
+ @names.dump
92
+ end
93
+
94
+ def parse_description(json)
95
+ Discovery::RestDescription::Representation.new(Discovery::RestDescription.new).from_json(json)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,339 @@
1
+ # Copyright 2015 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'logger'
16
+ require 'erb'
17
+ require 'yaml'
18
+ require 'json'
19
+ require 'active_support/inflector'
20
+ require 'google/apis/core/logging'
21
+ require 'google/apis/generator/template'
22
+ require 'google/apis/generator/model'
23
+ require 'google/apis/generator/helpers'
24
+ require 'addressable/uri'
25
+
26
+ module Google
27
+ module Apis
28
+
29
+ # @private
30
+ class Generator
31
+
32
+ # Helper for picking names for methods, properties, types, etc. Performs various normaliations
33
+ # as well as allows for overriding individual names from a configuration file for cases
34
+ # where algorithmic approaches produce poor APIs.
35
+ class Names
36
+ ActiveSupport::Inflector.inflections do |inflections|
37
+ inflections.uncountable('send_as', 'as')
38
+ inflections.irregular('drive', 'drives')
39
+ inflections.irregular('teamdrive', 'teamdrives')
40
+ end
41
+
42
+ include Google::Apis::Core::Logging
43
+ include NameHelpers
44
+
45
+ def initialize(names_out_file_path = nil, names_file_path = nil)
46
+ if names_out_file_path && File.file?(names_out_file_path)
47
+ logger.info { sprintf('Loading API names from %s', names_out_file_path) }
48
+ @names = YAML.load(File.read(names_out_file_path)) || {}
49
+ else
50
+ @names = {}
51
+ end
52
+ if names_file_path && File.file?(names_file_path)
53
+ logger.info { sprintf('Loading API names from %s', names_file_path) }
54
+ @names = @names.merge(YAML.load(File.read(names_file_path)) || {})
55
+ end
56
+ @path = []
57
+ end
58
+
59
+ def with_path(path)
60
+ @path.push(path)
61
+ begin
62
+ yield
63
+ ensure
64
+ @path.pop
65
+ end
66
+ end
67
+
68
+ def infer_parameter_name
69
+ pick_name(normalize_param_name(@path.last))
70
+ end
71
+
72
+ def infer_property_name
73
+ pick_name(normalize_property_name(@path.last))
74
+ end
75
+
76
+ def pick_name(alt_name)
77
+ preferred_name = @names[key]
78
+ if preferred_name && preferred_name == alt_name
79
+ # logger.warn { sprintf("Unnecessary name override '%s': %s", key, alt_name) }
80
+ elsif preferred_name.nil?
81
+ preferred_name = @names[key] = alt_name
82
+ end
83
+ preferred_name
84
+ end
85
+
86
+ def [](key)
87
+ @names[key]
88
+ end
89
+
90
+ def []=(key, value)
91
+ @names[key] = value
92
+ end
93
+
94
+ def dump
95
+ YAML.dump(Hash[@names.sort])
96
+ end
97
+
98
+ def key
99
+ @path.reduce('') { |a, e| a + '/' + e }
100
+ end
101
+
102
+ def option(opt_name)
103
+ @names[sprintf('%s?%s', key, opt_name)]
104
+ end
105
+
106
+ # For RPC style methods, pick a name based off the request objects.
107
+ # @param [Google::Apis::DiscoveryV1::RestMethod] method
108
+ # @param [Boolean] pick_name
109
+ # Fragment of the discovery doc describing the method
110
+ def infer_method_name_for_rpc(method, pick_name = true)
111
+ return nil if method.request.nil?
112
+ parts = method.id.split('.')
113
+ parts.shift
114
+ verb = ActiveSupport::Inflector.underscore(parts.pop)
115
+ match = method.request._ref.match(/(.*)(?i:request)/)
116
+ return nil if match.nil?
117
+ name = ActiveSupport::Inflector.underscore(match[1])
118
+ return nil unless name == verb || name.start_with?(verb + '_')
119
+ if !parts.empty?
120
+ resource_name = ActiveSupport::Inflector.singularize(parts.pop)
121
+ resource_name = ActiveSupport::Inflector.underscore(resource_name)
122
+ if !name.include?(resource_name)
123
+ name = name.split('_').insert(1, resource_name).join('_')
124
+ end
125
+ end
126
+ if pick_name
127
+ pick_name(name)
128
+ else
129
+ name
130
+ end
131
+ end
132
+
133
+ # For REST style methods, build a method name from the verb/resource(s) in the method
134
+ # id. IDs are in the form <api>.<resource>.<verb>
135
+ # @param [Google::Apis::DiscoveryV1::RestMethod] method
136
+ # Fragment of the discovery doc describing the method
137
+ def infer_method_name_from_id(method)
138
+ parts = method.id.split('.')
139
+ parts.shift
140
+ verb = ActiveSupport::Inflector.underscore(parts.pop)
141
+ return verb if parts.empty?
142
+ resource_name = ActiveSupport::Inflector.underscore(parts.pop)
143
+ if pluralize_method?(verb)
144
+ resource_name = ActiveSupport::Inflector.pluralize(resource_name)
145
+ else
146
+ resource_name = ActiveSupport::Inflector.singularize(resource_name)
147
+ end
148
+ if parts.empty?
149
+ resource_path = resource_name
150
+ else
151
+ resource_path = parts.map do |p|
152
+ p = ActiveSupport::Inflector.singularize(p)
153
+ ActiveSupport::Inflector.underscore(p)
154
+ end.join('_') + '_' + resource_name
155
+ end
156
+ method_name = verb.split('_').insert(1, resource_path.split('_')).join('_')
157
+ pick_name(method_name)
158
+ end
159
+ end
160
+
161
+ # Modifies an API description to support ruby code generation. Primarily does:
162
+ # - Ensure all names follow appopriate ruby conventions
163
+ # - Maps types to ruby types, classes, and resolves $refs
164
+ # - Attempts to simplify names where possible to make APIs more sensible
165
+ class Annotator
166
+ include NameHelpers
167
+ include Google::Apis::Core::Logging
168
+
169
+ # Prepare the API for the templates.
170
+ # @param [Google::Apis::DiscoveryV1::RestDescription] description
171
+ # API Description
172
+ def self.process(description, api_names = nil)
173
+ Annotator.new(description, api_names).annotate_api
174
+ end
175
+
176
+ # @param [Google::Apis::DiscoveryV1::RestDescription] description
177
+ # API Description
178
+ # @param [Google::Api::Generator::Names] api_names
179
+ # Name helper instanace
180
+ def initialize(description, api_names = nil)
181
+ api_names = Names.new if api_names.nil?
182
+ @names = api_names
183
+ @rest_description = description
184
+ @registered_types = []
185
+ @deferred_types = []
186
+ @strip_prefixes = []
187
+ @all_methods = {}
188
+ @dup_method_names_for_rpc = collect_dup_method_names_for_rpc
189
+ @path = []
190
+ end
191
+
192
+ def collect_method_names_for_rpc(resource, method_names_for_rpc)
193
+ resource.api_methods.each do |_k, v|
194
+ # First look for the method name in the `@names` hash. If there's
195
+ # no override set, generate it without inserting the generated name
196
+ # into the `@names` hash.
197
+ method_name_for_rpc = @names[@names.key]
198
+ if method_name_for_rpc.nil?
199
+ method_name_for_rpc = @names.infer_method_name_for_rpc(v, false)
200
+ end
201
+ method_names_for_rpc << method_name_for_rpc if method_name_for_rpc
202
+ end unless resource.api_methods.nil?
203
+
204
+ resource.resources.each do |_k, v|
205
+ collect_method_names_for_rpc(v, method_names_for_rpc)
206
+ end unless resource.resources.nil?
207
+ end
208
+
209
+ def collect_dup_method_names_for_rpc
210
+ method_names_for_rpc = []
211
+ collect_method_names_for_rpc(@rest_description, method_names_for_rpc)
212
+ method_names_for_rpc.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)
213
+ end
214
+
215
+ def annotate_api
216
+ @names.with_path(@rest_description.id) do
217
+ @strip_prefixes << @rest_description.name
218
+ if @rest_description.auth
219
+ @rest_description.auth.oauth2.scopes.each do |key, value|
220
+ value.constant = constantize_scope(key)
221
+ end
222
+ end
223
+ @rest_description.force_alt_json = @names.option('force_alt_json')
224
+ annotate_parameters(@rest_description.parameters)
225
+ annotate_resource(@rest_description.name, @rest_description)
226
+ @rest_description.schemas.each do |k, v|
227
+ annotate_type(k, v, @rest_description)
228
+ end unless @rest_description.schemas.nil?
229
+ end
230
+ resolve_type_references
231
+ resolve_variants
232
+ end
233
+
234
+ def annotate_type(name, type, parent)
235
+ @names.with_path(name) do
236
+ type.name = name
237
+ type.path = @names.key
238
+ type.generated_name = @names.infer_property_name
239
+ if type.type == 'object'
240
+ type.generated_class_name = ActiveSupport::Inflector.camelize(type.generated_name)
241
+ @registered_types << type
242
+ end
243
+ type.parent = parent
244
+ @deferred_types << type if type._ref
245
+ type.properties.each do |k, v|
246
+ annotate_type(k, v, type)
247
+ end unless type.properties.nil?
248
+ if type.additional_properties
249
+ type.type = 'hash'
250
+ annotate_type(ActiveSupport::Inflector.singularize(type.generated_name), type.additional_properties,
251
+ parent)
252
+ end
253
+ annotate_type(ActiveSupport::Inflector.singularize(type.generated_name), type.items, parent) if type.items
254
+ end
255
+ end
256
+
257
+ def annotate_resource(name, resource, parent_resource = nil)
258
+ @strip_prefixes << name
259
+ resource.parent = parent_resource unless parent_resource.nil?
260
+ resource.api_methods.each do |_k, v|
261
+ annotate_method(v, resource)
262
+ end unless resource.api_methods.nil?
263
+
264
+ resource.resources.each do |k, v|
265
+ annotate_resource(k, v, resource)
266
+ end unless resource.resources.nil?
267
+ end
268
+
269
+ def annotate_method(method, parent_resource = nil)
270
+ @names.with_path(method.id) do
271
+ method.parent = parent_resource
272
+ # Grab the method name generated from the request object without
273
+ # inserting into, or querying, the names hash.
274
+ method_name_for_rpc = @names.infer_method_name_for_rpc(method, false)
275
+ # If `method_name_for_rpc` is a duplicate (more than one method in
276
+ # the API will generate this name), generate the method name from
277
+ # the method ID instead.
278
+ if @dup_method_names_for_rpc.include?(method_name_for_rpc)
279
+ method.generated_name = @names.infer_method_name_from_id(method)
280
+ # Otherwise, proceed as normal.
281
+ elsif method_name_for_rpc
282
+ method.generated_name = @names.infer_method_name_for_rpc(method)
283
+ else
284
+ method.generated_name = @names.infer_method_name_from_id(method)
285
+ end
286
+ check_duplicate_method(method)
287
+ annotate_parameters(method.parameters)
288
+ end
289
+ end
290
+
291
+ def annotate_parameters(parameters)
292
+ parameters.each do |key, value|
293
+ @names.with_path(key) do
294
+ value.name = key
295
+ value.generated_name = @names.infer_parameter_name
296
+ @deferred_types << value if value._ref
297
+ end
298
+ end unless parameters.nil?
299
+ end
300
+
301
+ def resolve_type_references
302
+ @deferred_types.each do |type|
303
+ if type._ref
304
+ ref = @rest_description.schemas[type._ref]
305
+ ivars = ref.instance_variables - [:@name, :@generated_name]
306
+ (ivars).each do |var|
307
+ type.instance_variable_set(var, ref.instance_variable_get(var))
308
+ end
309
+ end
310
+ end
311
+ end
312
+
313
+ def resolve_variants
314
+ @deferred_types.each do |type|
315
+ if type.variant
316
+ type.variant.map.each do |v|
317
+ ref = @rest_description.schemas[v._ref]
318
+ ref.base_ref = type
319
+ ref.discriminant = type.variant.discriminant
320
+ ref.discriminant_value = v.type_value
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ def check_duplicate_method(m)
327
+ if @all_methods.include?(m.generated_name)
328
+ logger.error do
329
+ sprintf('Duplicate method %s generated, conflicting paths %s and %s',
330
+ m.generated_name, @names.key, @all_methods[m.generated_name])
331
+ end
332
+ fail 'Duplicate name generated'
333
+ end
334
+ @all_methods[m.generated_name] = @names.key
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,78 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Google
4
+ module Apis
5
+ # @private
6
+ class Generator
7
+ # Methods for validating & normalizing symbols
8
+ module NameHelpers
9
+ KEYWORDS = %w(__ENCODING__ def in self __LINE__ defined? module super __FILE__ do next then BEGIN
10
+ else nil true END elsif not undef alias end or unless and ensure redo until begin
11
+ false rescue when break for retry while case if return yield class command)
12
+ PLURAL_METHODS = %w(list search)
13
+
14
+ # Check to see if the method name should be plauralized
15
+ # @return [Boolean]
16
+ def pluralize_method?(method_name)
17
+ PLURAL_METHODS.include?(method_name)
18
+ end
19
+
20
+ # Check to see if the method is either a keyword or built-in method on object
21
+ # @return [Boolean]
22
+ def reserved?(name)
23
+ keyword?(name) || object_method?(name)
24
+ end
25
+
26
+ # Check to see if the name is a ruby keyword
27
+ # @return [Boolean]
28
+ def keyword?(name)
29
+ KEYWORDS.include?(name)
30
+ end
31
+
32
+ # Check to see if the method already exists on ruby objects
33
+ # @return [Boolean]
34
+ def object_method?(name)
35
+ Object.new.respond_to?(name)
36
+ end
37
+
38
+ # Convert a parameter name to ruby conventions
39
+ # @param [String] name
40
+ # @return [String] updated param name
41
+ def normalize_param_name(name)
42
+ name = name.gsub(/\W/, '_')
43
+ name = name.gsub(/IPv4/, 'Ipv4')
44
+ name = ActiveSupport::Inflector.underscore(name)
45
+ if reserved?(name)
46
+ logger.warn { sprintf('Found reserved keyword \'%1$s\'', name) }
47
+ name += '_'
48
+ logger.warn { sprintf('Changed to \'%1$s\'', name) }
49
+ end
50
+ name
51
+ end
52
+
53
+ # Convert a property name to ruby conventions
54
+ # @param [String] name
55
+ # @return [String]
56
+ def normalize_property_name(name)
57
+ name = ActiveSupport::Inflector.underscore(name.gsub(/\W/, '_'))
58
+ if object_method?(name)
59
+ logger.warn { sprintf('Found reserved property \'%1$s\'', name) }
60
+ name += '_prop'
61
+ logger.warn { sprintf('Changed to \'%1$s\'', name) }
62
+ end
63
+ name
64
+ end
65
+
66
+ # Converts a scope string into a ruby constant
67
+ # @param [String] url
68
+ # Url to convert
69
+ # @return [String]
70
+ def constantize_scope(url)
71
+ scope = Addressable::URI.parse(url).path[1..-1].upcase.gsub(/\W/, '_')
72
+ scope = 'AUTH_SCOPE' if scope.nil? || scope.empty?
73
+ scope
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end