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,3 @@
1
+ class MicrosoftGraph
2
+ CACHED_METADATA_DIRECTORY = File.realpath(File.join(__FILE__, "../../../data/"))
3
+ end
@@ -0,0 +1,217 @@
1
+ class MicrosoftGraph
2
+ class ClassBuilder
3
+ @@loaded = false
4
+
5
+ def self.load!(service)
6
+ if !@@loaded
7
+ @service_namespace = service.namespace
8
+ service.entity_types.each do |entity_type|
9
+ create_class! entity_type
10
+ end
11
+
12
+ service.complex_types.each do |complex_type|
13
+ create_class! complex_type
14
+ end
15
+
16
+ service.entity_sets.each do |entity_set|
17
+ add_graph_association! entity_set
18
+ end
19
+
20
+ service.actions.each do |action|
21
+ add_action_method! action
22
+ end
23
+
24
+ service.functions.each do |function|
25
+ add_function_method! function
26
+ end
27
+
28
+ service.singletons.each do |singleton|
29
+ class_name = classify(singleton.type_name)
30
+ MicrosoftGraph.instance_eval do
31
+ resource_name = singleton.name
32
+ define_method(OData.convert_to_snake_case(resource_name)) do
33
+ MicrosoftGraph
34
+ .const_get(class_name)
35
+ .new(
36
+ graph: self,
37
+ resource_name: resource_name,
38
+ parent: self
39
+ ).tap(&:fetch)
40
+ end
41
+ end
42
+ end
43
+
44
+ MicrosoftGraph.instance_eval do
45
+ define_method(:navigation_properties) do
46
+ service.entity_sets
47
+ .concat(service.singletons)
48
+ .map { |navigation_property|
49
+ [navigation_property.name.to_sym, navigation_property]
50
+ }.to_h
51
+ end
52
+ end
53
+
54
+ @@loaded = true
55
+ end
56
+ end
57
+
58
+ def self.loaded?
59
+ @@loaded
60
+ end
61
+
62
+ private
63
+
64
+ def remove_service_namespace(name)
65
+ name.gsub("#{@service_namespace}.", "")
66
+ end
67
+
68
+ def self.create_class!(type)
69
+ superklass = get_superklass(type)
70
+ klass = MicrosoftGraph.const_set(classify(type.name), Class.new(superklass))
71
+ klass.const_set("ODATA_TYPE", type)
72
+ klass.instance_eval do
73
+ def self.odata_type
74
+ const_get("ODATA_TYPE")
75
+ end
76
+ end
77
+ create_properties(klass, type)
78
+ create_navigation_properties(klass, type) if type.respond_to? :navigation_properties
79
+ end
80
+
81
+ def self.add_graph_association!(entity_set)
82
+ klass = get_namespaced_class(entity_set.member_type)
83
+ resource_name = entity_set.name.gsub("#{@service_namespace}.", "")
84
+ odata_collection =
85
+ OData::CollectionType.new(member_type: klass.odata_type, name: entity_set.name)
86
+ MicrosoftGraph.send(:define_method, resource_name) do
87
+ @association_collections[entity_set.name] ||=
88
+ MicrosoftGraph::CollectionAssociation
89
+ .new(
90
+ type: odata_collection,
91
+ resource_name: resource_name,
92
+ parent: self
93
+ )
94
+ .tap do |collection|
95
+ collection.graph = self
96
+ end
97
+ end
98
+ end
99
+
100
+ def self.create_properties(klass, type)
101
+ property_map = type.properties.map { |property|
102
+ define_getter_and_setter(klass, property)
103
+ [
104
+ OData.convert_to_snake_case(property.name).to_sym,
105
+ property
106
+ ]
107
+ }.to_h
108
+
109
+ klass.class_eval do
110
+ define_method(:properties) do
111
+ super().merge(property_map)
112
+ end
113
+ end
114
+ end
115
+
116
+ def self.define_getter_and_setter(klass, property)
117
+ klass.class_eval do
118
+ property_name = OData.convert_to_snake_case(property.name)
119
+ define_method(property_name.to_sym) do
120
+ get(property_name.to_sym)
121
+ end
122
+ define_method("#{property_name}=".to_sym) do |value|
123
+ set(property_name.to_sym, value)
124
+ end
125
+ end
126
+ end
127
+
128
+ def self.create_navigation_properties(klass, type)
129
+ klass.class_eval do
130
+ type.navigation_properties.each do |navigation_property|
131
+ navigation_property_name = OData.convert_to_snake_case(navigation_property.name).to_sym
132
+ define_method(navigation_property_name.to_sym) do
133
+ get_navigation_property(navigation_property_name.to_sym)
134
+ end
135
+ unless navigation_property.collection?
136
+ define_method("#{navigation_property_name}=".to_sym) do |value|
137
+ set_navigation_property(navigation_property_name.to_sym, value)
138
+ end
139
+ end
140
+ end
141
+
142
+ define_method(:navigation_properties) do
143
+ type.navigation_properties.map { |navigation_property|
144
+ [
145
+ OData.convert_to_snake_case(navigation_property.name).to_sym,
146
+ navigation_property
147
+ ]
148
+ }.to_h
149
+ end
150
+ end
151
+ end
152
+
153
+ def self.add_function_method!(function)
154
+ klass = get_namespaced_class(function.binding_type.name)
155
+ klass.class_eval do
156
+ define_method(OData.convert_to_snake_case(function.name).to_sym) do |params={}|
157
+ raise NoAssociationError unless parent
158
+ raise_no_graph_error! unless graph
159
+ function_params = params.map do |param_key, param_value|
160
+ "#{OData.convert_to_camel_case(param_key)}='#{param_value}'"
161
+ end
162
+ response = graph.service.get("#{path}/microsoft.graph.#{function.name}(#{function_params.join(',')})")
163
+ if function.return_type
164
+ if function.return_type.collection?
165
+ Collection.new(function.return_type, response[:attributes]['value'])
166
+ else
167
+ ClassBuilder.get_namespaced_class(function.return_type.name).new(attributes: response[:attributes]['value'])
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ def self.add_action_method!(action)
175
+ klass = get_namespaced_class(action.binding_type.name)
176
+ klass.class_eval do
177
+ define_method(OData.convert_to_snake_case(action.name).to_sym) do |args={}|
178
+ raise NoAssociationError unless parent
179
+ raise_no_graph_error! unless graph
180
+ response = graph.service.post("#{path}/#{action.name}", OData.convert_keys_to_camel_case(args).to_json)
181
+ if action.return_type
182
+ if action.return_type.collection?
183
+ Collection.new(action.return_type, response['value'])
184
+ else
185
+ ClassBuilder.get_namespaced_class(action.return_type.name).new(attributes: response['value'])
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ def self.classify(name)
193
+ raw_name = name.gsub("#{@service_namespace}.", "")
194
+ raw_name.to_s.slice(0, 1).capitalize + raw_name.to_s.slice(1..-1)
195
+ end
196
+
197
+ def self.get_namespaced_class(property_name)
198
+ klass_name = classify(property_name)
199
+ klass = begin
200
+ MicrosoftGraph.const_get(klass_name) if MicrosoftGraph.const_defined?(klass_name)
201
+ rescue NameError
202
+ return false
203
+ end
204
+ klass && MicrosoftGraph::BaseEntity != klass.superclass && klass
205
+ end
206
+
207
+ def self.get_superklass(type)
208
+ if type.base_type.nil?
209
+ (type.class == OData::ComplexType) ?
210
+ MicrosoftGraph::Base :
211
+ MicrosoftGraph::BaseEntity
212
+ else
213
+ Object.const_get("MicrosoftGraph::" + classify(type.base_type))
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,95 @@
1
+ require 'forwardable'
2
+
3
+ class MicrosoftGraph
4
+ class Collection
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ attr_accessor :graph
9
+
10
+ def_delegators :values, :size, :length, :include?, :empty?, :[]
11
+
12
+ def initialize(type, initial_values = [])
13
+ @type = type
14
+ @dirty = false
15
+ @internal_values =
16
+ if klass = MicrosoftGraph::ClassBuilder.get_namespaced_class(type.member_type.name)
17
+ initial_values.map { |v| klass.new(attributes: v) }
18
+ else
19
+ initial_values
20
+ end
21
+ end
22
+
23
+ def each
24
+ values.each do |value|
25
+ yield value
26
+ end
27
+ end
28
+
29
+ def <<(value)
30
+ unless @type.valid_value?(value)
31
+ raise MicrosoftGraph::TypeError.new(
32
+ "Value \"#{value}\" does not match type #{@type.member_type.name}"
33
+ )
34
+ end
35
+ @dirty = true
36
+ @internal_values << value
37
+ self
38
+ end
39
+
40
+ def []=(index, value)
41
+ raise MicrosoftGraph::TypeError unless @type.valid_value?(value)
42
+ @dirty = true
43
+ values[index] = value
44
+ end
45
+
46
+ def dirty?
47
+ @dirty || @internal_values.any? { |value|
48
+ value.respond_to?(:dirty?) && value.dirty?
49
+ }
50
+ end
51
+
52
+ def mark_clean
53
+ @dirty = false
54
+ @internal_values.each { |value|
55
+ value.respond_to?(:mark_clean) && value.mark_clean
56
+ }
57
+ end
58
+
59
+ def as_json(options = {})
60
+ values.map do |value|
61
+ if value.respond_to? :as_json
62
+ value.as_json(options)
63
+ else
64
+ value
65
+ end
66
+ end
67
+ end
68
+
69
+ def new(attributes = {})
70
+ klass = MicrosoftGraph::ClassBuilder.get_namespaced_class(@type.member_type.name)
71
+ if klass
72
+ new_instance = klass.new(graph: graph, attributes: attributes)
73
+ self << new_instance
74
+ new_instance
75
+ else
76
+ raise NoMethodError.new("undefined method `new' for #{as_json}:#{self.class}")
77
+ end
78
+ end
79
+
80
+ def parental_chain
81
+ if parent && parent.respond_to?(:parental_chain)
82
+ parent.parental_chain.concat([parent])
83
+ else
84
+ [parent]
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def values
91
+ @internal_values
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,230 @@
1
+ class MicrosoftGraph
2
+ class CollectionAssociation < Collection
3
+ attr_reader :type
4
+ attr_reader :parent
5
+
6
+ def initialize(options = {})
7
+ @type = options[:type]
8
+ @graph = options[:graph]
9
+ @resource_name = options[:resource_name]
10
+ @parent = options[:parent]
11
+ @order_by = options[:order_by]
12
+ @filter = options[:filter]
13
+ @dirty = false
14
+ @loaded = false
15
+ @internal_values = []
16
+
17
+ raise MicrosoftGraph::TypeError.new("A collection cannot be both ordered and filtered.") if @order_by && @filter
18
+ @order_by && @order_by.each do |field|
19
+ field_name, direction = field.to_s.split(' ')
20
+ field_names = field_name.split("/")
21
+ unless valid_nested_property(field_names, @type)
22
+ raise MicrosoftGraph::TypeError.new("Tried to order by invalid field: #{field_name}")
23
+ end
24
+ if direction && ! %w(asc desc).include?(direction)
25
+ raise MicrosoftGraph::TypeError.new("Tried to order field #{field_name} by invalid direction: #{direction}")
26
+ end
27
+ end
28
+ @filter && @filter.respond_to?(:keys) && @filter.keys.each do |field_name|
29
+ unless @type.properties.map(&:name).include? OData.convert_to_camel_case(field_name)
30
+ raise MicrosoftGraph::TypeError.new("Tried to filter by invalid field: #{field_name}")
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ def find(id)
37
+ if response = graph.service.get("#{path}/#{URI.escape(id.to_s)}")
38
+ klass = if member_type = specified_member_type(response)
39
+ ClassBuilder.get_namespaced_class(response)
40
+ else
41
+ default_member_class
42
+ end
43
+ klass.new(
44
+ graph: graph,
45
+ parent: self,
46
+ attributes: response[:attributes],
47
+ persisted: true
48
+ )
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ def path
55
+ if parent && parent.path
56
+ "#{parent.path}/#{@resource_name}"
57
+ else
58
+ @resource_name
59
+ end
60
+ end
61
+
62
+ def query_path
63
+ if @order_by
64
+ order_by_names = @order_by.map do |field|
65
+ URI.escape OData.convert_to_camel_case(field)
66
+ end
67
+ "#{path}?$orderby=#{order_by_names.join(',')}"
68
+ elsif @filter
69
+ escaped_filters = URI.escape(stringify_filters(@filter))
70
+ "#{path}?$filter=#{escaped_filters}"
71
+ else
72
+ path
73
+ end
74
+ end
75
+
76
+ def order_by(*fields)
77
+ self.class.new(
78
+ type: @type,
79
+ graph: @graph,
80
+ resource_name: @resource_name,
81
+ parent: @parent,
82
+ order_by: fields,
83
+ filter: @filter
84
+ )
85
+ end
86
+
87
+ def valid_nested_property(field_names, parent_type)
88
+ field_name = field_names.shift
89
+ if field_name
90
+ property = parent_type.properties.find do |prop|
91
+ OData.convert_to_camel_case(prop.name) == OData.convert_to_camel_case(field_name)
92
+ end
93
+ property && valid_nested_property(field_names, property.type)
94
+ else
95
+ true
96
+ end
97
+ end
98
+
99
+ def filter(new_filters)
100
+ self.class.new(
101
+ type: @type,
102
+ graph: @graph,
103
+ resource_name: @resource_name,
104
+ parent: @parent,
105
+ order_by: @order_by,
106
+ filter: merge_with_existing_filter(new_filters)
107
+ )
108
+ end
109
+
110
+ def merge_with_existing_filter(new_filters)
111
+ existing_filter = @filter || {}
112
+ if new_filters.respond_to?(:keys) && existing_filter.respond_to?(:keys)
113
+ existing_filter.merge(new_filters)
114
+ else
115
+ [stringify_filters(new_filters), stringify_filters(@filter)].compact.join(" and ")
116
+ end
117
+ end
118
+
119
+ def stringify_filters(filters)
120
+ if filters.respond_to?(:keys)
121
+ filter_strings = filters.map do |k,v|
122
+ v = v.is_a?(String) ? "'#{v}'" : v
123
+ "#{OData.convert_to_camel_case(k)} eq #{v}"
124
+ end
125
+ filter_strings.sort.join(' and ')
126
+ else
127
+ filters
128
+ end
129
+ end
130
+
131
+ def create(attributes = {})
132
+ build(attributes).tap { |n| n.save }
133
+ end
134
+
135
+ def create!(attributes = {})
136
+ build(attributes).tap { |n| n.save! }
137
+ end
138
+
139
+ def build(attributes = {})
140
+ ClassBuilder
141
+ .get_namespaced_class(type.member_type.name)
142
+ .new(
143
+ graph: graph,
144
+ resource_name: @resource_name,
145
+ parent: self,
146
+ attributes: attributes,
147
+ ).tap { |new_member|
148
+ @internal_values << new_member
149
+ }
150
+ end
151
+
152
+ def <<(new_member)
153
+ super
154
+ new_member.graph = graph
155
+ new_member.parent = self
156
+ new_member.save
157
+ self
158
+ end
159
+
160
+ def each(start = 0)
161
+ return to_enum(:each, start) unless block_given?
162
+ @next_link = query_path
163
+ Array(@internal_values[start..-1]).each do |element|
164
+ yield(element)
165
+ end
166
+
167
+ unless last?
168
+ start = [@internal_values.size, start].max
169
+
170
+ fetch_next_page
171
+
172
+ each(start, &Proc.new)
173
+ end
174
+ end
175
+
176
+ def collection?
177
+ true
178
+ end
179
+
180
+ def reload!
181
+ @next_link = query_path
182
+ @internal_values = []
183
+ fetch_next_page
184
+ end
185
+
186
+ private
187
+
188
+ def last?
189
+ @loaded || @internal_values.size >= 1000
190
+ end
191
+
192
+ def fetch_next_page
193
+ @next_link ||= query_path
194
+ result = begin
195
+ @graph.service.get(@next_link)
196
+ rescue OData::ClientError => e
197
+ if matches = /Unsupported sort property '([^']*)'/.match(e.message)
198
+ raise MicrosoftGraph::TypeError.new("Cannot sort by #{matches[1]}")
199
+ elsif /OrderBy not supported/.match(e.message)
200
+ if @order_by.length == 1
201
+ raise MicrosoftGraph::TypeError.new("Cannot sort by #{@order_by.first}")
202
+ else
203
+ raise MicrosoftGraph::TypeError.new("Cannot sort by at least one field requested")
204
+ end
205
+ else
206
+ raise e
207
+ end
208
+ end
209
+ @next_link = result[:attributes]['@odata.next_link']
210
+ result[:attributes]['value'].each do |entity_hash|
211
+ klass =
212
+ if member_type = specified_member_type(entity_hash)
213
+ ClassBuilder.get_namespaced_class(member_type.name)
214
+ else
215
+ default_member_class
216
+ end
217
+ @internal_values.push klass.new(attributes: entity_hash, parent: self, persisted: true)
218
+ end
219
+ @loaded = result[:attributes]['@odata.next_link'].nil?
220
+ end
221
+
222
+ def default_member_class
223
+ ClassBuilder.get_namespaced_class(type.member_type.name)
224
+ end
225
+
226
+ def specified_member_type(entity_hash)
227
+ @graph.service.get_type_for_odata_response(entity_hash)
228
+ end
229
+ end
230
+ end