Basic structure of schema parsing complete.
[arvados.git] / lib / google / api_client / discovery / schema.rb
1 # Copyright 2010 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
16 require 'time'
17 require 'json'
18 require 'base64'
19 require 'addressable/uri'
20 require 'addressable/template'
21
22 require 'google/inflection'
23 require 'google/api_client/errors'
24
25 module Google
26   class APIClient
27     module Schema
28       def self.parse(api, schema_data)
29         # This method is super-long, but hard to break up due to the
30         # unavoidable dependence on closures and execution context.
31         schema_name = schema_data['id']
32
33         if schema_name
34           api_name_string =
35             Google::INFLECTOR.camelize(api.name)
36           api_version_string =
37             Google::INFLECTOR.camelize(api.version).gsub('.', '_')
38           if Google::APIClient::Schema.const_defined?(api_name_string)
39             api_name = Google::APIClient::Schema.const_get(api_name_string)
40           else
41             api_name = Google::APIClient::Schema.const_set(
42               api_name_string, Module.new
43             )
44           end
45           if api_name.const_defined?(api_version_string)
46             api_version = api_name.const_get(api_version_string)
47           else
48             api_version = api_name.const_set(api_version_string, Module.new)
49           end
50           if api_version.const_defined?(schema_name)
51             schema_class = api_version.const_get(schema_name)
52           end
53         end
54
55         # It's possible the schema has already been defined. If so, don't
56         # redefine it. This means that reloading a schema which has already
57         # been loaded into memory is not possible.
58         unless schema_class
59           schema = self
60           schema_class = Class.new(APIObject) do |klass|
61             properties = []
62             define_method('schema') do
63               schema_data
64             end
65             (schema_data['properties'] || []).each do |(k, v)|
66               property_name = Google::INFLECTOR.underscore(k)
67               properties << property_name.to_sym
68               define_method(:schema) { schema }
69               define_method(property_name + '_schema') do
70                 v
71               end
72               define_method(property_name + '_description') do
73                 v['description']
74               end
75               case v['type']
76               when 'string'
77                 define_string_property(api, property_name, k, v)
78               when 'boolean'
79                 define_boolean_property(api, property_name, k, v)
80               when 'number'
81                 define_number_property(api, property_name, k, v)
82               when 'array'
83                 define_array_property(api, property_name, k, v)
84               when 'object'
85                 define_object_property(api, property_name, k, v)
86               else
87                 # Either type 'any' or we don't know what this is,
88                 # default to anything goes.
89                 define_any_property(api, property_name, k, v)
90               end
91             end
92
93             define_method('properties') do
94               properties
95             end
96           end
97           if schema_name
98             api_version.const_set(schema_name, schema_class)
99           end
100         end
101         return schema_class
102       end
103     end
104
105     class APIObject
106       def self.define_string_property(api, property_name, key, schema_data)
107         define_method(property_name) do
108           self[key] ||= schema_data['default']
109           if schema_data['format'] == 'byte' && self[key] != nil
110             Base64.decode64(self[key])
111           elsif schema_data['format'] == 'date-time' && self[key] != nil
112             Time.parse(self[key])
113           elsif schema_data['format'] =~ /^u?int(32|64)$/ && self[key] != nil
114             self[key].to_i
115           else
116             self[key]
117           end
118         end
119         define_method(property_name + '=') do |value|
120           if schema_data['format'] == 'byte'
121             self[key] = Base64.encode64(value)
122           elsif schema_data['format'] == 'date-time'
123             if value.respond_to?(:to_str)
124               value = Time.parse(value.to_str)
125             elsif !value.respond_to?(:xmlschema)
126               raise TypeError,
127                 "Could not obtain RFC 3339 timestamp from #{value.class}."
128             end
129             self[key] = value.xmlschema
130           elsif schema_data['format'] =~ /^u?int(32|64)$/
131             self[key] = value.to_s
132           elsif value.respond_to?(:to_str)
133             self[key] = value.to_str
134           elsif value.kind_of?(Symbol)
135             self[key] = value.to_s
136           else
137             raise TypeError,
138               "Expected String or Symbol, got #{value.class}."
139           end
140         end
141       end
142
143       def self.define_boolean_property(api, property_name, key, schema_data)
144         define_method(property_name) do
145           self[key] ||= schema_data['default']
146           case self[key].to_s.downcase
147           when 'true', 'yes', 'y', 'on', '1'
148             true
149           when 'false', 'no', 'n', 'off', '0'
150             false
151           when 'nil', 'null'
152             nil
153           else
154             raise TypeError,
155               "Expected boolean, got #{self[key].class}."
156           end
157         end
158         define_method(property_name + '=') do |value|
159           case value.to_s.downcase
160           when 'true', 'yes', 'y', 'on', '1'
161             self[key] = true
162           when 'false', 'no', 'n', 'off', '0'
163             self[key] = false
164           when 'nil', 'null'
165             self[key] = nil
166           else
167             raise TypeError, "Expected boolean, got #{value.class}."
168           end
169         end
170       end
171
172       def self.define_number_property(api, property_name, key, schema_data)
173         define_method(property_name) do
174           self[key] ||= schema_data['default']
175           if self[key] != nil && !self[key].respond_to?(:to_f)
176             raise TypeError,
177               "Expected Float, got #{self[key].class}."
178           elsif self[key] != nil && self[key].respond_to?(:to_f)
179             self[key].to_f
180           else
181             self[key]
182           end
183         end
184         define_method(property_name + '=') do |value|
185           if value == nil
186             self[key] = value
187           else
188             case schema_data['format']
189             when 'double', 'float'
190               if value.respond_to?(:to_f)
191                 self[key] = value.to_f
192               else
193                 raise TypeError,
194                   "Expected String or Symbol, got #{value.class}."
195               end
196             else
197               raise TypeError,
198                 "Unexpected type format for number: #{schema_data['format']}."
199             end
200           end
201         end
202       end
203
204       def self.define_array_property(api, property_name, key, schema_data)
205         define_method(property_name) do
206           # The default value of an empty Array obviates a mutator method.
207           self[key] ||= []
208           array = if self[key] != nil && !self[key].respond_to?(:to_ary)
209             raise TypeError,
210               "Expected Array, got #{self[key].class}."
211           else
212             self[key].to_ary
213           end
214           if schema_data['items'] && schema_data['items']['$ref']
215             schema_name = schema_data['items']['$ref']
216             if api.schemas[schema_name]
217               schema_class = api.schemas[schema_name]
218               array.map! do |item|
219                 schema_class.new(item)
220               end
221             else
222               raise ArgumentError,
223                 "Could not find schema '#{schema_name}' in API '#{api.id}'."
224             end
225           end
226           array
227         end
228       end
229
230       def self.define_object_property(api, property_name, key, schema_data)
231         # TODO finish this up...
232         schema = Schema.parse(api, schema_data)
233         define_method(property_name) do
234           self[key] ||= v['default']
235           schema.new(self[key])
236         end
237       end
238
239       def self.define_any_property(api, property_name, key, schema_data)
240         define_method(property_name) do
241           self[key] ||= v['default']
242         end
243         define_method(property_name + '=') do |value|
244           self[key] = value
245         end
246       end
247
248       def initialize(data)
249         @data = data
250       end
251
252       def [](key)
253         return @data[key]
254       end
255
256       def []=(key, value)
257         return @data[key] = value
258       end
259     end
260   end
261 end