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,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