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,18 @@
1
+ require "common_spec_helper"
2
+ require "dotenv"
3
+ Dotenv.load
4
+ # ADAL::Logging.log_level = ADAL::Logger::VERBOSE
5
+
6
+ TENANT = ENV['MS_GRAPH_TENANT']
7
+ USERNAME = ENV['MS_GRAPH_USERNAME']
8
+ PASSWORD = ENV['MS_GRAPH_PASSWORD']
9
+ CLIENT_ID = ENV['MS_GRAPH_CLIENT_ID']
10
+ CLIENT_SECRET = ENV['MS_GRAPH_CLIENT_SECRET']
11
+ RESOURCE = 'https://graph.microsoft.com'
12
+
13
+ USER_CRED = ADAL::UserCredential.new(USERNAME, PASSWORD)
14
+ CLIENT_CRED = ADAL::ClientCredential.new(CLIENT_ID, CLIENT_SECRET)
15
+ CONTEXT = ADAL::AuthenticationContext.new(ADAL::Authority::WORLD_WIDE_AUTHORITY, TENANT)
16
+ TOKENS = CONTEXT.acquire_token_for_user(RESOURCE, CLIENT_CRED, USER_CRED)
17
+
18
+ create_classes(TOKENS)
@@ -0,0 +1,180 @@
1
+ require_relative "integration_spec_helper"
2
+
3
+ describe MicrosoftGraph::User do
4
+ Given(:auth_callback) {
5
+ Proc.new { |r| r.headers["Authorization"] = "Bearer #{TOKENS.access_token}" }
6
+ }
7
+ Given(:test_run_id) { rand(2**128) }
8
+ Given(:graph) { MicrosoftGraph.new(&auth_callback) }
9
+ Given(:user) { graph.users.take(3).last }
10
+ Given(:email_destination) { user.user_principal_name }
11
+ Given(:message_template) {
12
+ {
13
+ subject: "test message #{test_run_id}",
14
+ body: {
15
+ content: "Hello.\n\nThis message is generated by an automated test suite.",
16
+ },
17
+ to_recipients: [
18
+ { email_address: { address: email_destination } },
19
+ ],
20
+ }
21
+ }
22
+
23
+ describe 'current user' do
24
+ Given(:subject) { graph.me }
25
+
26
+ describe 'direct reports' do
27
+ When(:result) { subject.direct_reports.take(5) }
28
+ Then { result.length == 0 }
29
+ end
30
+
31
+ describe 'membership' do
32
+ Given(:groups) { subject.member_of.take(5) }
33
+ Given(:group) { groups.last }
34
+
35
+ When(:result) { subject.check_member_groups(group_ids: [group.id]) }
36
+
37
+ Then { result.to_a == [group.id] }
38
+ And { groups.length == 5 }
39
+ And { group.display_name.length > 0 }
40
+ end
41
+
42
+ describe MicrosoftGraph::Drive do
43
+ Given(:drive) { subject.drive }
44
+
45
+ describe MicrosoftGraph::DriveItem do
46
+ Given(:root) { drive.root }
47
+ Given(:root_contents) { root.children }
48
+
49
+ Then { root_contents.size == 0 }
50
+ end
51
+ end
52
+
53
+ describe 'contacts' do
54
+ Given(:contacts) { subject.contacts.take(5) }
55
+ Given(:contact) { contacts.last }
56
+
57
+ Then { contacts.to_a.size == 5 }
58
+ And { contact.display_name.length > 0 }
59
+ end
60
+
61
+ describe 'email' do
62
+ describe 'send a new email' do
63
+ When(:result) { subject.send_mail(message: message_template) }
64
+ Then { result != Failure() }
65
+ end
66
+ end
67
+
68
+ describe 'messages' do
69
+ Given(:messages) { subject.mail_folders.find('Inbox').messages }
70
+ Given(:first_five_messages) { messages.take(5) }
71
+ Given(:message) { first_five_messages.last }
72
+
73
+ describe 'list' do
74
+ When(:result) { first_five_messages.size }
75
+ Then { result == 5 }
76
+ end
77
+
78
+ describe 'post a reply' do
79
+ When(:result) { message.create_reply('test reply') }
80
+ Then { result != Failure() }
81
+ end
82
+
83
+ describe 'post a reply-all' do
84
+ When(:result) { message.create_reply_all('test reply-all') }
85
+ Then { result != Failure() }
86
+ end
87
+
88
+ describe 'drafts' do
89
+ Given(:draft_messages) { subject.mail_folders.find('Drafts').messages }
90
+ # Note: Graph API seems to not allow you to find a mail_folder with a space in its name like we do above
91
+ Given(:sent_messages) { subject.mail_folders.detect { |f| f.display_name == 'Sent Items' }.messages }
92
+
93
+ describe 'post and send a draft message' do
94
+ When(:draft_message) { draft_messages.create!(message_template) }
95
+ When(:draft_id) { draft_message.id }
96
+ When(:draft_title) { draft_message.subject }
97
+ When(:send_result) { draft_message.send }
98
+ When { sleep 0.5 }
99
+ When(:try_finding_in_drafts) { draft_messages.find(draft_id) }
100
+ # below could find the wrong message if someone else is sending at the same time:
101
+ When(:sent_message) { sent_messages.order_by('sentDateTime desc').first }
102
+ When(:sent_title) { sent_message.subject }
103
+
104
+ Then { send_result != Failure() }
105
+ And { try_finding_in_drafts == Failure(OData::ClientError, /404/) }
106
+ And { sent_title == draft_title }
107
+ end
108
+ end
109
+ end
110
+
111
+ describe 'calendar' do
112
+ Given(:calendar) { subject.calendar }
113
+ Given(:event_template) {
114
+ {
115
+ subject: 'test event',
116
+ body: {
117
+ content: 'this event generated by an automated test suite'
118
+ },
119
+ }
120
+ }
121
+
122
+ describe 'events' do
123
+ Given(:events) { calendar.events }
124
+
125
+ describe 'new' do
126
+
127
+ describe 'create!' do
128
+ When(:event) { events.create!(event_template) }
129
+ When(:id) { event.id }
130
+ When(:title) { event.subject }
131
+ When { event.delete! }
132
+ When(:get_deleted_event) { events.find(id) }
133
+
134
+ Then { title == event_template[:subject] }
135
+ And { get_deleted_event == Failure(OData::ClientError, /404/) }
136
+ end
137
+
138
+ describe 'create recurring' do
139
+ Given(:start_date) { Date.today }
140
+ Given(:recurring_event_template) {
141
+ event_template.merge(
142
+ recurrence: {
143
+ pattern: {
144
+ days_of_week: [start_date.strftime('%A').downcase],
145
+ interval: 1,
146
+ type: 'weekly',
147
+ },
148
+ range: {
149
+ start_date: start_date,
150
+ type: 'noEnd',
151
+ },
152
+ }
153
+ )
154
+ }
155
+ When(:event) { events.create!(recurring_event_template) }
156
+ When(:id) { event.id }
157
+ When(:title) { event.subject }
158
+ When { event.delete! }
159
+ When(:get_deleted_event) { events.find(id) }
160
+
161
+ Then { id.length > 0 }
162
+ And { title == event_template[:subject] }
163
+ And { get_deleted_event == Failure(OData::ClientError, /404/) }
164
+ end
165
+
166
+ describe 'add attachment'
167
+ end
168
+
169
+ describe 'existing' do
170
+ describe 'first invitation' do
171
+ describe 'tentatively accept'
172
+ describe 'accept'
173
+ describe 'decline'
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ end
@@ -0,0 +1,35 @@
1
+ require "odata"
2
+
3
+ Dir[
4
+ File.join(
5
+ File.dirname(__FILE__),
6
+ 'microsoft_graph',
7
+ '*'
8
+ )
9
+ ].each { |f| require f }
10
+
11
+ class MicrosoftGraph
12
+ attr_reader :service
13
+ BASE_URL = "https://graph.microsoft.com/v1.0/"
14
+
15
+ def initialize(options = {}, &auth_callback)
16
+ @service = OData::Service.new(
17
+ base_url: BASE_URL,
18
+ metadata_file: options[:cached_metadata_file],
19
+ auth_callback: auth_callback
20
+ )
21
+ @association_collections = {}
22
+ unless MicrosoftGraph::ClassBuilder.loaded?
23
+ MicrosoftGraph::ClassBuilder.load!(service)
24
+ end
25
+
26
+ end
27
+
28
+ def containing_navigation_property(type_name)
29
+ navigation_properties.values.find do |navigation_property|
30
+ navigation_property.collection? && navigation_property.type.name == "Collection(#{type_name})"
31
+ end
32
+ end
33
+
34
+ def path; end
35
+ end
@@ -0,0 +1,110 @@
1
+ class MicrosoftGraph
2
+ class Base
3
+
4
+ def initialize(options = {})
5
+ @cached_navigation_property_values = {}
6
+ @cached_property_values = {}
7
+ if options[:attributes]
8
+ initialize_serialized_properties(options[:attributes], options[:persisted])
9
+ end
10
+ @dirty = ! options[:persisted]
11
+ @dirty_properties = if @dirty
12
+ @cached_property_values.keys.inject({}) do |result, key|
13
+ result[key] = true
14
+ result
15
+ end
16
+ else
17
+ {}
18
+ end
19
+ end
20
+
21
+ def properties
22
+ {}
23
+ end
24
+
25
+ def odata_type
26
+ self.class.const_get("ODATA_TYPE").name
27
+ end
28
+
29
+ def as_json(options = {})
30
+ (if options[:only]
31
+ @cached_property_values.select { |key,v| options[:only].include? key }
32
+ elsif options[:except]
33
+ @cached_property_values.reject { |key,v| options[:except].include? key }
34
+ else
35
+ @cached_property_values
36
+ end).inject({}) do |result, (k,v)|
37
+ k = OData.convert_to_camel_case(k) if options[:convert_to_camel_case]
38
+ result[k.to_s] = v.respond_to?(:as_json) ? v.as_json(options) : v
39
+ result
40
+ end
41
+ end
42
+
43
+ def to_json(options = {})
44
+ as_json(options).to_json
45
+ end
46
+
47
+ def dirty?
48
+ @dirty || @cached_property_values.any? { |key, value|
49
+ value.respond_to?(:dirty?) && value.dirty?
50
+ }
51
+ end
52
+
53
+ def mark_clean
54
+ @dirty = false
55
+ @dirty_properties = {}
56
+ @cached_property_values.each { |key, value|
57
+ value.respond_to?(:mark_clean) && value.mark_clean
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def get(property_name)
64
+ if properties[property_name].collection?
65
+ @cached_property_values[property_name] ||= Collection.new(properties[property_name].type)
66
+ else
67
+ @cached_property_values[property_name]
68
+ end
69
+ end
70
+
71
+ def set(property_name, value)
72
+ property = properties[property_name]
73
+
74
+ raise NonNullableError unless property.nullable_match?(value)
75
+ if property.collection?
76
+ raise TypeError unless value.all? { |v| property.collection_type_match?(v) }
77
+ @cached_property_values[property_name] = Collection.new(property.type, value)
78
+ else
79
+ raise TypeError unless property.type_match?(value)
80
+ @cached_property_values[property_name] = property.coerce_to_type(value)
81
+ end
82
+ @dirty = true
83
+ @dirty_properties[property_name] = true
84
+ end
85
+
86
+ def initialize_serialized_properties(raw_attributes, from_server = false)
87
+ unless raw_attributes.respond_to? :keys
88
+ raise TypeError.new("Cannot initialize #{self.class} with attributes: #{raw_attributes.inspect}")
89
+ end
90
+ attributes = OData.convert_keys_to_snake_case(raw_attributes)
91
+ properties.each do |property_key, property|
92
+ if attributes.keys.include?(property_key.to_s)
93
+ value = attributes[property_key.to_s]
94
+ @cached_property_values[property_key] =
95
+ if property.collection?
96
+ Collection.new(property.type, value)
97
+ elsif klass = MicrosoftGraph::ClassBuilder.get_namespaced_class(property.type.name)
98
+ klass.new(attributes: value)
99
+ else
100
+ if from_server && ! property.type_match?(value) && OData::EnumType === property.type
101
+ value.to_s
102
+ else
103
+ property.coerce_to_type(value)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,152 @@
1
+ class MicrosoftGraph
2
+ class BaseEntity < Base
3
+
4
+ attr_accessor :graph
5
+ attr_accessor :parent
6
+
7
+ def initialize(options = {})
8
+ @resource_name = options[:resource_name]
9
+ @parent = options[:parent] || options[:graph]
10
+ @graph = options[:graph] || parent && parent.graph
11
+ @navigation_property_name = options[:navigation_property_name]
12
+ @persisted = options[:persisted] || false
13
+ super
14
+ end
15
+
16
+ def parental_chain
17
+ if parent && parent.respond_to?(:parental_chain)
18
+ parent.parental_chain.concat([parent])
19
+ else
20
+ [parent]
21
+ end
22
+ end
23
+
24
+ def containing_navigation_property(type_name)
25
+ candidate_navigation_properties = navigation_properties.values.select do |navigation_property|
26
+ navigation_property.collection? && navigation_property.type.name == "Collection(#{type_name})"
27
+ end
28
+ candidate_navigation_properties.sort { |a, b|
29
+ a_index = type_name.downcase.index(a.name[0..-2].downcase) || 0
30
+ b_index = type_name.downcase.index(b.name[0..-2].downcase) || 0
31
+ a_index <=> b_index
32
+ }.last
33
+ end
34
+
35
+ def path
36
+ containing_navigation_property_name = nil
37
+ owning_ancestor = parental_chain.find do |ancestor|
38
+ unless MicrosoftGraph::CollectionAssociation === ancestor
39
+ containing_navigation_property = ancestor.containing_navigation_property(odata_type)
40
+ containing_navigation_property && containing_navigation_property_name = containing_navigation_property.name
41
+ end
42
+ end
43
+
44
+ if owning_ancestor && @cached_property_values[:id]
45
+ [owning_ancestor.path, containing_navigation_property_name, @cached_property_values[:id]].compact.join("/")
46
+ else
47
+ @resource_name
48
+ end
49
+ end
50
+
51
+ def fetch
52
+ @persisted = true
53
+ initialize_serialized_properties(graph.service.get(path)[:attributes])
54
+ end
55
+
56
+ def persisted?
57
+ @persisted
58
+ end
59
+
60
+ def delete!
61
+ if persisted?
62
+ @persisted = false
63
+ graph.service.delete(path)
64
+ end
65
+ end
66
+
67
+ def reload!
68
+ @dirty_properties.keys.each do |dirty_property|
69
+ @cached_property_values[dirty_property] = nil
70
+ end
71
+ mark_clean
72
+ fetch if persisted?
73
+ end
74
+
75
+ def save!
76
+ raise NoAssociationError unless parent
77
+ raise_no_graph_error! unless graph
78
+ if persisted?
79
+ graph.service.patch(path, to_json(only: @dirty_properties.keys, convert_to_camel_case: true))
80
+ else
81
+ initialize_serialized_properties(
82
+ graph.service.post(parent.path, to_json(convert_to_camel_case: true))
83
+ )
84
+ @persisted = true
85
+ end
86
+ mark_clean
87
+ true
88
+ end
89
+
90
+ def save
91
+ save!
92
+ rescue OData::HTTPError
93
+ false
94
+ end
95
+
96
+ private
97
+
98
+ def raise_no_graph_error!
99
+ raise NoGraphError.new("#{self.class}#graph must be a MicrosoftGraph instance to make network requests.")
100
+ end
101
+
102
+ def get(property_name)
103
+ if uncached_property?(property_name) && graph
104
+ initialize_serialized_properties(graph.service.get(path, property_name.to_s)[:attributes], true)
105
+ super
106
+ else
107
+ super
108
+ end
109
+ end
110
+
111
+ def get_navigation_property(navigation_property_name)
112
+ raise_no_graph_error! unless graph
113
+ navigation_property = navigation_properties[navigation_property_name]
114
+ if navigation_property.collection?
115
+ @cached_navigation_property_values[navigation_property_name] ||=
116
+ CollectionAssociation.new(
117
+ graph: graph,
118
+ type: navigation_properties[navigation_property_name].type,
119
+ resource_name: OData.convert_to_camel_case(navigation_property_name.to_s),
120
+ parent: self
121
+ )
122
+ else
123
+ @cached_navigation_property_values[navigation_property_name] ||=
124
+ if response = graph.service.get("#{path}/#{OData.convert_to_camel_case(navigation_property_name.to_s)}")
125
+ type = graph.service.get_type_for_odata_response(response[:attributes]) || navigation_property.type
126
+ klass = ClassBuilder.get_namespaced_class(type.name)
127
+ klass.new(
128
+ graph: graph,
129
+ parent: self,
130
+ attributes: response[:attributes],
131
+ persisted: true,
132
+ navigation_property_name: navigation_property_name.to_s
133
+ )
134
+ else
135
+ nil
136
+ end
137
+ end
138
+ end
139
+
140
+ def set_navigation_property(navigation_property_name, value)
141
+ navigation_property = navigation_properties[navigation_property_name]
142
+ raise TypeError unless navigation_property.type_match?(value)
143
+ value.parent = self
144
+ @cached_navigation_property_values[navigation_property_name] = value
145
+ end
146
+
147
+ def uncached_property?(property)
148
+ properties.keys.include?(property) && ! @cached_property_values.keys.include?(property)
149
+ end
150
+
151
+ end
152
+ end