microsoft_graph 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.
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