microsoft_graph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +10 -0
  7. data/README.md +97 -0
  8. data/Rakefile +7 -0
  9. data/data/metadata_v1.0.xml +1687 -0
  10. data/integration_spec/integration_spec_helper.rb +18 -0
  11. data/integration_spec/live_spec.rb +180 -0
  12. data/lib/microsoft_graph.rb +35 -0
  13. data/lib/microsoft_graph/base.rb +110 -0
  14. data/lib/microsoft_graph/base_entity.rb +152 -0
  15. data/lib/microsoft_graph/cached_metadata_directory.rb +3 -0
  16. data/lib/microsoft_graph/class_builder.rb +217 -0
  17. data/lib/microsoft_graph/collection.rb +95 -0
  18. data/lib/microsoft_graph/collection_association.rb +230 -0
  19. data/lib/microsoft_graph/errors.rb +6 -0
  20. data/lib/microsoft_graph/version.rb +3 -0
  21. data/lib/odata.rb +49 -0
  22. data/lib/odata/entity_set.rb +20 -0
  23. data/lib/odata/errors.rb +18 -0
  24. data/lib/odata/navigation_property.rb +30 -0
  25. data/lib/odata/operation.rb +17 -0
  26. data/lib/odata/property.rb +38 -0
  27. data/lib/odata/request.rb +48 -0
  28. data/lib/odata/service.rb +280 -0
  29. data/lib/odata/singleton.rb +20 -0
  30. data/lib/odata/type.rb +25 -0
  31. data/lib/odata/types/collection_type.rb +30 -0
  32. data/lib/odata/types/complex_type.rb +19 -0
  33. data/lib/odata/types/entity_type.rb +33 -0
  34. data/lib/odata/types/enum_type.rb +37 -0
  35. data/lib/odata/types/primitive_type.rb +12 -0
  36. data/lib/odata/types/primitive_types/binary_type.rb +15 -0
  37. data/lib/odata/types/primitive_types/boolean_type.rb +15 -0
  38. data/lib/odata/types/primitive_types/date_time_offset_type.rb +15 -0
  39. data/lib/odata/types/primitive_types/date_type.rb +23 -0
  40. data/lib/odata/types/primitive_types/double_type.rb +16 -0
  41. data/lib/odata/types/primitive_types/guid_type.rb +24 -0
  42. data/lib/odata/types/primitive_types/int_16_type.rb +19 -0
  43. data/lib/odata/types/primitive_types/int_32_type.rb +15 -0
  44. data/lib/odata/types/primitive_types/int_64_type.rb +15 -0
  45. data/lib/odata/types/primitive_types/stream_type.rb +15 -0
  46. data/lib/odata/types/primitive_types/string_type.rb +15 -0
  47. data/microsoft_graph.gemspec +31 -0
  48. data/tasks/update_metadata.rb +17 -0
  49. metadata +232 -0
@@ -0,0 +1,6 @@
1
+ class MicrosoftGraph
2
+ class TypeError < ::TypeError; end
3
+ class NonNullableError < TypeError; end
4
+ class NoGraphError < ::RuntimeError; end
5
+ class NoAssociationError < ::RuntimeError; end
6
+ end
@@ -0,0 +1,3 @@
1
+ class MicrosoftGraph
2
+ VERSION = "0.1.0"
3
+ end
data/lib/odata.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'date'
2
+
3
+ Dir[
4
+ File.join(
5
+ File.dirname(__FILE__),
6
+ 'odata',
7
+ '*.rb'
8
+ )
9
+ ].each { |f| require f }
10
+
11
+ module OData
12
+
13
+ def self.convert_to_snake_case(str)
14
+ first_letter, rest = str.to_s.split("", 2)
15
+ "#{first_letter}#{rest.to_s.gsub(/([A-Z])/, '_\1')}".downcase
16
+ end
17
+
18
+ def self.convert_to_camel_case(str)
19
+ first_letter, rest = str.to_s.split("", 2)
20
+ cameled_rest = rest.gsub(/_(.)/) { |l| l[1].upcase }
21
+ first_letter.downcase.concat(cameled_rest)
22
+ end
23
+
24
+ def self.convert_keys_to_snake_case(properties)
25
+ if properties.respond_to? :keys
26
+ results = {}
27
+ properties.each do |key, value|
28
+ results[convert_to_snake_case(key)] = convert_keys_to_snake_case(value)
29
+ end
30
+ results
31
+ else
32
+ properties
33
+ end
34
+ end
35
+
36
+ def self.convert_keys_to_camel_case(properties)
37
+ if properties.respond_to? :keys
38
+ results = {}
39
+ properties.each do |key, value|
40
+ results[convert_to_camel_case(key)] = convert_keys_to_camel_case(value)
41
+ end
42
+ results
43
+ elsif properties.is_a? Array
44
+ properties.map { |m| convert_keys_to_camel_case(m) }
45
+ else
46
+ properties
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ module OData
2
+ class EntitySet
3
+ attr_reader :name
4
+ attr_reader :member_type
5
+
6
+ def initialize(options = {})
7
+ @name = options[:name]
8
+ @member_type = options[:member_type]
9
+ @service = options[:service]
10
+ end
11
+
12
+ def collection?
13
+ true
14
+ end
15
+
16
+ def type
17
+ @service.get_type_by_name("Collection(#{member_type})")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ module OData
2
+ class Error < RuntimeError; end
3
+
4
+ class HTTPError < Error
5
+ def initialize(response)
6
+ body = JSON.parse(response.body)
7
+ super "#{response.code} #{body['error']['code']}: \"#{body['error']['message']}\" from \"#{response.uri}\""
8
+ rescue
9
+ super
10
+ end
11
+ end
12
+
13
+ class ClientError < HTTPError; end
14
+ class ServerError < HTTPError; end
15
+
16
+ class AuthenticationError < ClientError; end
17
+ class AuthorizationError < ClientError; end
18
+ end
@@ -0,0 +1,30 @@
1
+ module OData
2
+ class NavigationProperty
3
+ attr_reader :name
4
+ attr_reader :type
5
+ attr_reader :nullable
6
+ attr_reader :contain_target
7
+ attr_reader :partner
8
+
9
+ def initialize(options = {})
10
+ @name = options[:name]
11
+ @type = options[:type]
12
+ @nullable = options[:nullable].nil? ? true : false
13
+ @contain_target = options[:contain_target] || false
14
+ @partner = options[:partner]
15
+ end
16
+
17
+ def collection?
18
+ OData::CollectionType === type
19
+ end
20
+
21
+ def type_match?(value)
22
+ type.valid_value?(value)
23
+ end
24
+
25
+ def collection_type_match?(value)
26
+ collection = type
27
+ collection.valid_value?(value)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module OData
2
+ class Operation
3
+ attr_reader :name
4
+ attr_reader :binding_type
5
+ attr_reader :entity_set_type
6
+ attr_reader :parameters
7
+ attr_reader :return_type
8
+
9
+ def initialize(options = {})
10
+ @name = options[:name]
11
+ @entity_set_type = options[:entity_set_type]
12
+ @binding_type = options[:binding_type]
13
+ @parameters = options[:parameters]
14
+ @return_type = options[:return_type]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ require 'date'
2
+
3
+ module OData
4
+ class Property
5
+ attr_reader :name
6
+ attr_reader :nullable
7
+ attr_reader :type
8
+
9
+ def initialize(options = {})
10
+ @name = options[:name]
11
+ @nullable = options[:nullable]
12
+ @type = options[:type]
13
+ end
14
+
15
+ def collection?
16
+ OData::CollectionType === type
17
+ end
18
+
19
+ def coerce_to_type(value)
20
+ return nil if value.nil?
21
+ type.coerce(value)
22
+ end
23
+
24
+ def collection_type_match?(value)
25
+ collection = type
26
+ collection.valid_value?(value)
27
+ end
28
+
29
+ def type_match?(value)
30
+ type.valid_value?(value) || (value.nil? && nullable)
31
+ end
32
+
33
+ def nullable_match?(value)
34
+ nullable || !value.nil?
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ module OData
2
+ class Request
3
+ attr_accessor :headers
4
+ attr_accessor :uri
5
+ attr_accessor :method
6
+ attr_accessor :data
7
+
8
+ def initialize(method = :get, uri = '', data = nil)
9
+ @method = method.to_s.downcase.to_sym
10
+ @uri = URI(uri)
11
+ @data = data
12
+ @headers = {
13
+ 'Content-Type' => 'application/json'
14
+ }
15
+ end
16
+
17
+ def perform
18
+ response = Net::HTTP
19
+ .new(uri.hostname, uri.port)
20
+ .tap { |h| h.use_ssl = true }
21
+ .send(*send_params)
22
+ raise ServerError.new(response) unless response.code.to_i < 500
23
+ raise AuthenticationError.new(response) if response.code.to_i == 401
24
+ raise AuthorizationError.new(response) if response.code.to_i == 403
25
+ raise ClientError.new(response) unless response.code.to_i < 400
26
+ if response.body
27
+ begin
28
+ OData.convert_keys_to_snake_case(JSON.parse(response.body))
29
+ rescue JSON::ParserError => e
30
+ {}
31
+ end
32
+ else
33
+ {}
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def send_params
40
+ base_params = [
41
+ method,
42
+ uri
43
+ ]
44
+ base_params << data if data
45
+ base_params << headers
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,280 @@
1
+ module OData
2
+ class Service
3
+ attr_reader :base_url
4
+ attr_reader :metadata
5
+
6
+ def initialize(options = {}, &block)
7
+ @auth_callback = options[:auth_callback] || block
8
+ @base_url = options[:base_url]
9
+ @metadata_file = options[:metadata_file]
10
+ @type_name_map = {}
11
+ @metadata = fetch_metadata
12
+ populate_types_from_metadata
13
+ end
14
+
15
+ def namespace
16
+ schema_defintion = metadata.xpath("//Schema") && metadata.xpath("//Schema").first
17
+ schema_defintion["Namespace"] if schema_defintion
18
+ end
19
+
20
+ def inspect
21
+ "#<#{self.class} #{base_url}>"
22
+ end
23
+
24
+ def get(path, *select_properties)
25
+ camel_case_select_properties = select_properties.map do |prop|
26
+ OData.convert_to_camel_case(prop)
27
+ end
28
+
29
+ if ! camel_case_select_properties.empty?
30
+ encoded_select_properties = URI.encode_www_form(
31
+ '$select' => camel_case_select_properties.join(',')
32
+ )
33
+ path = "#{path}?#{encoded_select_properties}"
34
+ end
35
+
36
+ response = request(
37
+ method: :get,
38
+ uri: "#{base_url}#{path}"
39
+ )
40
+ {type: get_type_for_odata_response(response), attributes: response}
41
+ end
42
+
43
+ def delete(path)
44
+ request(
45
+ method: :delete,
46
+ uri: "#{base_url}#{path}"
47
+ )
48
+ end
49
+
50
+ def post(path, data)
51
+ request(
52
+ method: :post,
53
+ uri: "#{base_url}#{path}",
54
+ data: data
55
+ )
56
+ end
57
+
58
+ def patch(path, data)
59
+ request(
60
+ method: :patch,
61
+ uri: "#{base_url}#{path}",
62
+ data: data
63
+ )
64
+ end
65
+
66
+ def request(options = {})
67
+ req = Request.new(options[:method], options[:uri], options[:data])
68
+ @auth_callback.call(req) if @auth_callback
69
+ req.perform
70
+ end
71
+
72
+ def complex_types
73
+ @complex_types ||= metadata.xpath("//ComplexType").map do |complex_type|
74
+ @type_name_map["#{namespace}.#{complex_type["Name"]}"] = ComplexType.new(
75
+ name: "#{namespace}.#{complex_type["Name"]}",
76
+ base_type: complex_type["BaseType"],
77
+ service: self,
78
+ )
79
+ end
80
+ end
81
+
82
+ def entity_types
83
+ @entity_types ||= metadata.xpath("//EntityType").map do |entity|
84
+ options = {
85
+ name: "#{namespace}.#{entity["Name"]}",
86
+ abstract: entity["Abstract"] == "true",
87
+ base_type: entity["BaseType"],
88
+ open_type: entity["OpenType"] == "true",
89
+ has_stream: entity["HasStream"] == "true",
90
+ service: self,
91
+ }
92
+ @type_name_map["#{namespace}.#{entity["Name"]}"] = EntityType.new(options)
93
+ end
94
+ end
95
+
96
+ def enum_types
97
+ @enum_types ||= metadata.xpath("//EnumType").map do |type|
98
+ members = type.xpath("./Member").map do |m, i|
99
+ value = m['Value'] && m['Value'].to_i || i
100
+ {
101
+ name: m["Name"],
102
+ value: value,
103
+ }
104
+ end
105
+ @type_name_map["#{namespace}.#{type["Name"]}"] = EnumType.new({name: "#{namespace}.#{type["Name"]}", members: members})
106
+ end
107
+ end
108
+
109
+ def actions
110
+ metadata.xpath("//Action").map do |action|
111
+ build_operation(action)
112
+ end
113
+ end
114
+
115
+ def functions
116
+ metadata.xpath("//Function").map do |function|
117
+ build_operation(function)
118
+ end
119
+ end
120
+
121
+ def populate_primitive_types
122
+ @type_name_map.merge!(
123
+ "Edm.Binary" => OData::BinaryType.new,
124
+ "Edm.Date" => OData::DateType.new,
125
+ "Edm.Double" => OData::DoubleType.new,
126
+ "Edm.Guid" => OData::GuidType.new,
127
+ "Edm.Int16" => OData::Int16Type.new,
128
+ "Edm.Int32" => OData::Int32Type.new,
129
+ "Edm.Int64" => OData::Int64Type.new,
130
+ "Edm.Stream" => OData::StreamType.new,
131
+ "Edm.String" => OData::StringType.new,
132
+ "Edm.Boolean" => OData::BooleanType.new,
133
+ "Edm.DateTimeOffset" => OData::DateTimeOffsetType.new
134
+ )
135
+ end
136
+
137
+ def singletons
138
+ metadata.xpath("//Singleton").map do |singleton|
139
+ Singleton.new(
140
+ name: singleton["Name"],
141
+ type: singleton["Type"],
142
+ service: self
143
+ )
144
+ end
145
+ end
146
+
147
+ def entity_sets
148
+ @entity_sets ||= metadata.xpath("//EntitySet").map do |entity_set|
149
+ EntitySet.new(
150
+ name: entity_set["Name"],
151
+ member_type: entity_set["EntityType"],
152
+ service: self
153
+ )
154
+ end
155
+ end
156
+
157
+ def get_type_for_odata_response(parsed_response)
158
+ if odata_type_string = parsed_response["@odata.type"]
159
+ get_type_by_name(type_name_from_odata_type_field(odata_type_string))
160
+ elsif context = parsed_response["@odata.context"]
161
+ singular, segments = segments_from_odata_context_field(context)
162
+ first_entity_type = get_type_by_name("Collection(#{entity_set_by_name(segments.shift).member_type})")
163
+ entity_type = segments.reduce(first_entity_type) do |last_entity_type, segment|
164
+ last_entity_type.member_type.navigation_property_by_name(segment).type
165
+ end
166
+ singular && entity_type.respond_to?(:member_type) ? entity_type.member_type : entity_type
167
+ end
168
+ end
169
+
170
+ def get_type_by_name(type_name)
171
+ @type_name_map[type_name] || build_collection(type_name)
172
+ end
173
+
174
+ def entity_set_by_name(name)
175
+ entity_sets.find { |entity_set| entity_set.name == name }
176
+ end
177
+
178
+ def properties_for_type(type_name)
179
+ raw_type_name = remove_namespace(type_name)
180
+ type_definition = metadata.xpath("//EntityType[@Name='#{raw_type_name}']|//ComplexType[@Name='#{raw_type_name}']")
181
+ type_definition.xpath("./Property").map do |property|
182
+ options = {
183
+ name: property["Name"],
184
+ nullable: property["Nullable"] != "false",
185
+ type: get_type_by_name(property["Type"]),
186
+ }
187
+ OData::Property.new(options)
188
+ end
189
+ end
190
+
191
+ def navigation_properties_for_type(type_name)
192
+ raw_type_name = remove_namespace(type_name)
193
+ type_definition = metadata.xpath("//EntityType[@Name='#{raw_type_name}']|//ComplexType[@Name='#{raw_type_name}']")
194
+ type_definition.xpath("./NavigationProperty").map do |property|
195
+ options = {
196
+ name: property["Name"],
197
+ nullable: property["Nullable"] != "false",
198
+ type: get_type_by_name(property["Type"]),
199
+ contains_target: property["ContainsTarget"],
200
+ partner: property["Partner"],
201
+ }
202
+ OData::NavigationProperty.new(options)
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def type_name_from_odata_type_field(odata_type_field)
209
+ odata_type_field.sub("#", '')
210
+ end
211
+
212
+ def segments_from_odata_context_field(odata_context_field)
213
+ segments = odata_context_field.split("$metadata#").last.split("/").map { |s| s.split("(").first }
214
+ segments.pop if singular = segments.last == "$entity"
215
+ [singular, segments]
216
+ end
217
+
218
+ def populate_types_from_metadata
219
+ enum_types
220
+ populate_primitive_types
221
+ complex_types
222
+ entity_types
223
+ end
224
+
225
+ def fetch_metadata
226
+ response = if @metadata_file
227
+ File.read(@metadata_file)
228
+ else # From a URL
229
+ uri = URI("#{base_url}$metadata?detailed=true")
230
+ Net::HTTP
231
+ .new(uri.hostname, uri.port)
232
+ .tap { |h| h.use_ssl = uri.scheme == "https" }
233
+ .get(uri).body
234
+ end
235
+ ::Nokogiri::XML(response).remove_namespaces!
236
+ end
237
+
238
+ def build_collection(collection_name)
239
+ member_type_name = collection_name.gsub(/Collection\(([^)]+)\)/, "\\1")
240
+ CollectionType.new(name: collection_name, member_type: @type_name_map[member_type_name])
241
+ end
242
+
243
+ def build_operation(operation_xml)
244
+ binding_type = if operation_xml["IsBound"] == "true"
245
+ binding.pry if operation_xml.xpath("./Parameter[@Name='bindingParameter']|./Parameter[@Name='bindingparameter']").length == 0
246
+ type_name = operation_xml.xpath("./Parameter[@Name='bindingParameter']|./Parameter[@Name='bindingparameter']").first["Type"]
247
+ get_type_by_name(type_name)
248
+ end
249
+ entity_set_type = if operation_xml["EntitySetType"]
250
+ entity_set_by_name(operation_xml["EntitySetType"])
251
+ end
252
+ parameters = operation_xml.xpath("./Parameter").inject([]) do |result, parameter|
253
+ unless parameter["Name"] == 'bindingParameter'
254
+ result.push({
255
+ name: parameter["Name"],
256
+ type: get_type_by_name(parameter["Type"]),
257
+ nullable: parameter["Nullable"],
258
+ })
259
+ end
260
+ result
261
+ end
262
+ return_type = if return_type_node = operation_xml.xpath("./ReturnType").first
263
+ get_type_by_name(return_type_node["Type"])
264
+ end
265
+
266
+ options = {
267
+ name: operation_xml["Name"],
268
+ entity_set_type: entity_set_type,
269
+ binding_type: binding_type,
270
+ parameters: parameters,
271
+ return_type: return_type
272
+ }
273
+ Operation.new(options)
274
+ end
275
+
276
+ def remove_namespace(name)
277
+ name.gsub("#{namespace}.", "")
278
+ end
279
+ end
280
+ end