google-apis-generator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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