frodo 0.10.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 (153) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.circleci/config.yml +54 -0
  4. data/.gitignore +24 -0
  5. data/.gitlab-ci.yml +9 -0
  6. data/.rspec +2 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +75 -0
  10. data/CHANGELOG.md +163 -0
  11. data/Gemfile +4 -0
  12. data/LICENSE.txt +23 -0
  13. data/README.md +479 -0
  14. data/Rakefile +7 -0
  15. data/TODO.md +55 -0
  16. data/frodo.gemspec +39 -0
  17. data/images/frodo.jpg +0 -0
  18. data/lib/frodo/abstract_client.rb +11 -0
  19. data/lib/frodo/client.rb +6 -0
  20. data/lib/frodo/concerns/api.rb +292 -0
  21. data/lib/frodo/concerns/authentication.rb +32 -0
  22. data/lib/frodo/concerns/base.rb +84 -0
  23. data/lib/frodo/concerns/caching.rb +26 -0
  24. data/lib/frodo/concerns/connection.rb +79 -0
  25. data/lib/frodo/concerns/verbs.rb +68 -0
  26. data/lib/frodo/config.rb +143 -0
  27. data/lib/frodo/entity.rb +335 -0
  28. data/lib/frodo/entity_container.rb +75 -0
  29. data/lib/frodo/entity_set.rb +131 -0
  30. data/lib/frodo/errors.rb +70 -0
  31. data/lib/frodo/middleware/authentication/token.rb +13 -0
  32. data/lib/frodo/middleware/authentication.rb +87 -0
  33. data/lib/frodo/middleware/authorization.rb +18 -0
  34. data/lib/frodo/middleware/caching.rb +30 -0
  35. data/lib/frodo/middleware/custom_headers.rb +14 -0
  36. data/lib/frodo/middleware/gzip.rb +33 -0
  37. data/lib/frodo/middleware/instance_url.rb +20 -0
  38. data/lib/frodo/middleware/logger.rb +42 -0
  39. data/lib/frodo/middleware/multipart.rb +64 -0
  40. data/lib/frodo/middleware/odata_headers.rb +13 -0
  41. data/lib/frodo/middleware/raise_error.rb +47 -0
  42. data/lib/frodo/middleware.rb +33 -0
  43. data/lib/frodo/navigation_property/proxy.rb +80 -0
  44. data/lib/frodo/navigation_property.rb +29 -0
  45. data/lib/frodo/properties/binary.rb +50 -0
  46. data/lib/frodo/properties/boolean.rb +37 -0
  47. data/lib/frodo/properties/collection.rb +50 -0
  48. data/lib/frodo/properties/complex.rb +114 -0
  49. data/lib/frodo/properties/date.rb +27 -0
  50. data/lib/frodo/properties/date_time.rb +83 -0
  51. data/lib/frodo/properties/date_time_offset.rb +17 -0
  52. data/lib/frodo/properties/decimal.rb +54 -0
  53. data/lib/frodo/properties/enum.rb +62 -0
  54. data/lib/frodo/properties/float.rb +67 -0
  55. data/lib/frodo/properties/geography/base.rb +162 -0
  56. data/lib/frodo/properties/geography/line_string.rb +33 -0
  57. data/lib/frodo/properties/geography/point.rb +31 -0
  58. data/lib/frodo/properties/geography/polygon.rb +38 -0
  59. data/lib/frodo/properties/geography.rb +13 -0
  60. data/lib/frodo/properties/guid.rb +17 -0
  61. data/lib/frodo/properties/integer.rb +107 -0
  62. data/lib/frodo/properties/number.rb +14 -0
  63. data/lib/frodo/properties/string.rb +72 -0
  64. data/lib/frodo/properties/time.rb +40 -0
  65. data/lib/frodo/properties/time_of_day.rb +27 -0
  66. data/lib/frodo/properties.rb +32 -0
  67. data/lib/frodo/property.rb +139 -0
  68. data/lib/frodo/property_registry.rb +41 -0
  69. data/lib/frodo/query/criteria/comparison_operators.rb +49 -0
  70. data/lib/frodo/query/criteria/date_functions.rb +61 -0
  71. data/lib/frodo/query/criteria/geography_functions.rb +21 -0
  72. data/lib/frodo/query/criteria/lambda_operators.rb +27 -0
  73. data/lib/frodo/query/criteria/string_functions.rb +40 -0
  74. data/lib/frodo/query/criteria.rb +92 -0
  75. data/lib/frodo/query/in_batches.rb +58 -0
  76. data/lib/frodo/query.rb +221 -0
  77. data/lib/frodo/railtie.rb +19 -0
  78. data/lib/frodo/schema/complex_type.rb +79 -0
  79. data/lib/frodo/schema/enum_type.rb +95 -0
  80. data/lib/frodo/schema.rb +164 -0
  81. data/lib/frodo/service.rb +199 -0
  82. data/lib/frodo/service_registry.rb +52 -0
  83. data/lib/frodo/version.rb +3 -0
  84. data/lib/frodo.rb +67 -0
  85. data/spec/fixtures/auth_success_response.json +11 -0
  86. data/spec/fixtures/error.json +11 -0
  87. data/spec/fixtures/files/entity_to_xml.xml +18 -0
  88. data/spec/fixtures/files/error.xml +5 -0
  89. data/spec/fixtures/files/metadata.xml +150 -0
  90. data/spec/fixtures/files/metadata_with_error.xml +157 -0
  91. data/spec/fixtures/files/product_0.json +10 -0
  92. data/spec/fixtures/files/product_0.xml +28 -0
  93. data/spec/fixtures/files/products.json +106 -0
  94. data/spec/fixtures/files/products.xml +308 -0
  95. data/spec/fixtures/files/supplier_0.json +26 -0
  96. data/spec/fixtures/files/supplier_0.xml +32 -0
  97. data/spec/fixtures/leads.json +923 -0
  98. data/spec/fixtures/refresh_error_response.json +8 -0
  99. data/spec/frodo/abstract_client_spec.rb +13 -0
  100. data/spec/frodo/client_spec.rb +57 -0
  101. data/spec/frodo/concerns/authentication_spec.rb +79 -0
  102. data/spec/frodo/concerns/base_spec.rb +68 -0
  103. data/spec/frodo/concerns/caching_spec.rb +40 -0
  104. data/spec/frodo/concerns/connection_spec.rb +65 -0
  105. data/spec/frodo/config_spec.rb +127 -0
  106. data/spec/frodo/entity/shared_examples.rb +83 -0
  107. data/spec/frodo/entity_container_spec.rb +38 -0
  108. data/spec/frodo/entity_set_spec.rb +169 -0
  109. data/spec/frodo/entity_spec.rb +153 -0
  110. data/spec/frodo/errors_spec.rb +48 -0
  111. data/spec/frodo/middleware/authentication/token_spec.rb +87 -0
  112. data/spec/frodo/middleware/authentication_spec.rb +83 -0
  113. data/spec/frodo/middleware/authorization_spec.rb +17 -0
  114. data/spec/frodo/middleware/custom_headers_spec.rb +21 -0
  115. data/spec/frodo/middleware/gzip_spec.rb +68 -0
  116. data/spec/frodo/middleware/instance_url_spec.rb +27 -0
  117. data/spec/frodo/middleware/logger_spec.rb +21 -0
  118. data/spec/frodo/middleware/odata_headers_spec.rb +15 -0
  119. data/spec/frodo/middleware/raise_error_spec.rb +66 -0
  120. data/spec/frodo/navigation_property/proxy_spec.rb +46 -0
  121. data/spec/frodo/navigation_property_spec.rb +55 -0
  122. data/spec/frodo/properties/binary_spec.rb +50 -0
  123. data/spec/frodo/properties/boolean_spec.rb +72 -0
  124. data/spec/frodo/properties/collection_spec.rb +44 -0
  125. data/spec/frodo/properties/date_spec.rb +23 -0
  126. data/spec/frodo/properties/date_time_offset_spec.rb +30 -0
  127. data/spec/frodo/properties/date_time_spec.rb +23 -0
  128. data/spec/frodo/properties/decimal_spec.rb +50 -0
  129. data/spec/frodo/properties/float_spec.rb +45 -0
  130. data/spec/frodo/properties/geography/line_string_spec.rb +33 -0
  131. data/spec/frodo/properties/geography/point_spec.rb +29 -0
  132. data/spec/frodo/properties/geography/polygon_spec.rb +55 -0
  133. data/spec/frodo/properties/geography/shared_examples.rb +72 -0
  134. data/spec/frodo/properties/guid_spec.rb +17 -0
  135. data/spec/frodo/properties/integer_spec.rb +58 -0
  136. data/spec/frodo/properties/string_spec.rb +46 -0
  137. data/spec/frodo/properties/time_of_day_spec.rb +23 -0
  138. data/spec/frodo/properties/time_spec.rb +15 -0
  139. data/spec/frodo/property_registry_spec.rb +16 -0
  140. data/spec/frodo/property_spec.rb +71 -0
  141. data/spec/frodo/query/criteria_spec.rb +229 -0
  142. data/spec/frodo/query_spec.rb +156 -0
  143. data/spec/frodo/schema/complex_type_spec.rb +97 -0
  144. data/spec/frodo/schema/enum_type_spec.rb +112 -0
  145. data/spec/frodo/schema_spec.rb +113 -0
  146. data/spec/frodo/service_registry_spec.rb +19 -0
  147. data/spec/frodo/service_spec.rb +153 -0
  148. data/spec/frodo/usage_example_spec.rb +161 -0
  149. data/spec/spec_helper.rb +35 -0
  150. data/spec/support/coverage.rb +2 -0
  151. data/spec/support/fixture_helpers.rb +14 -0
  152. data/spec/support/middleware.rb +19 -0
  153. metadata +479 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Frodo
6
+ class << self
7
+ attr_writer :log
8
+
9
+ # Returns the current Configuration
10
+ #
11
+ # Example
12
+ #
13
+ # Frodo.configuration.username = "username"
14
+ # Frodo.configuration.password = "password"
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ # Yields the Configuration
20
+ #
21
+ # Example
22
+ #
23
+ # Frodo.configure do |config|
24
+ # config.username = "username"
25
+ # config.password = "password"
26
+ # end
27
+ def configure
28
+ yield configuration
29
+ end
30
+
31
+ def log?
32
+ @log ||= false
33
+ end
34
+
35
+ def log(message)
36
+ return unless Frodo.log?
37
+ configuration.logger.send(configuration.log_level, message)
38
+ end
39
+ end
40
+
41
+ class Configuration
42
+ class Option
43
+ attr_reader :configuration, :name, :options
44
+
45
+ def self.define(*args)
46
+ new(*args).define
47
+ end
48
+
49
+ def initialize(configuration, name, options = {})
50
+ @configuration = configuration
51
+ @name = name
52
+ @options = options
53
+ @default = options.fetch(:default, nil)
54
+ end
55
+
56
+ def define
57
+ write_attribute
58
+ define_method if default_provided?
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :default
65
+ alias default_provided? default
66
+
67
+ def write_attribute
68
+ configuration.send :attr_accessor, name
69
+ end
70
+
71
+ def define_method
72
+ our_default = default
73
+ our_name = name
74
+ configuration.send :define_method, our_name do
75
+ instance_variable_get(:"@#{our_name}") ||
76
+ instance_variable_set(
77
+ :"@#{our_name}",
78
+ our_default.respond_to?(:call) ? our_default.call : our_default
79
+ )
80
+ end
81
+ end
82
+ end
83
+
84
+ class << self
85
+ attr_accessor :options
86
+
87
+ def option(*args)
88
+ option = Option.define(self, *args)
89
+ (self.options ||= []) << option.name
90
+ end
91
+ end
92
+
93
+ # The OAuth client id
94
+ option :client_id
95
+
96
+ # The OAuth client secret
97
+ option :client_secret
98
+
99
+ option :host, default: 'login.microsoftonline.com'
100
+
101
+ option :oauth_token
102
+ option :refresh_token
103
+ option :instance_url
104
+ option :base_path
105
+
106
+ # Set this to an object that responds to read, write and fetch and all GET
107
+ # requests will be cached.
108
+ option :cache
109
+
110
+ # The number of times reauthentication should be tried before failing.
111
+ option :authentication_retries, default: 3
112
+
113
+ # Set to true if you want responses from Dynamics to be gzip compressed.
114
+ option :compress
115
+
116
+ # Faraday request read/open timeout.
117
+ option :timeout
118
+
119
+ # Faraday adapter to use. Defaults to Faraday.default_adapter.
120
+ option :adapter, default: lambda { Faraday.default_adapter }
121
+
122
+ option :proxy_uri, default: lambda { ENV['FRODATA_PROXY_URI'] }
123
+
124
+ # A Proc that is called with the response body after a successful authentication.
125
+ option :authentication_callback
126
+
127
+ # Set SSL options
128
+ option :ssl, default: {}
129
+
130
+ # A Hash that is converted to HTTP headers
131
+ option :request_headers
132
+
133
+ # Set a logger for when Frodo.log is set to true, defaulting to STDOUT
134
+ option :logger, default: ::Logger.new(STDOUT)
135
+
136
+ # Set a log level for logging when Frodo.log is set to true, defaulting to :debug
137
+ option :log_level, default: :debug
138
+
139
+ def options
140
+ self.class.options
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,335 @@
1
+ module Frodo
2
+ # An Frodo::Entity represents a single record returned by the service. All
3
+ # Entities have a type and belong to a specific namespace. They are written
4
+ # back to the service via the EntitySet they came from. Frodo::Entity
5
+ # instances should not be instantiated directly; instead, they should either
6
+ # be read or instantiated from their respective Frodo::EntitySet.
7
+ class Entity
8
+ # The Entity type name
9
+ attr_reader :type
10
+ # The Frodo::Service's identifying name
11
+ attr_reader :service_name
12
+ # The entity set this entity belongs to
13
+ attr_reader :entity_set
14
+ # List of errors on entity
15
+ attr_reader :errors
16
+
17
+ PROPERTY_NOT_LOADED = :not_loaded
18
+
19
+ XML_NAMESPACES = {
20
+ 'xmlns' => 'http://www.w3.org/2005/Atom',
21
+ 'xmlns:data' => 'http://docs.oasis-open.org/odata/ns/data',
22
+ 'xmlns:metadata' => 'http://docs.oasis-open.org/odata/ns/metadata',
23
+ 'xmlns:georss' => 'http://www.georss.org/georss',
24
+ 'xmlns:gml' => 'http://www.opengis.net/gml',
25
+ }.freeze
26
+
27
+ # Initializes a bare Entity
28
+ # @param options [Hash]
29
+ def initialize(options = {})
30
+ @id = options[:id]
31
+ @type = options[:type]
32
+ @service_name = options[:service_name]
33
+ @entity_set = options[:entity_set]
34
+ @context = options[:context]
35
+ @links = options[:links]
36
+ @errors = []
37
+ end
38
+
39
+ def namespace
40
+ @namespace ||= type.rpartition('.').first
41
+ end
42
+
43
+ # Returns name of Entity from Service specified type.
44
+ # @return [String]
45
+ def name
46
+ @name ||= type.split('.').last
47
+ end
48
+
49
+ # Returns context URL for this entity
50
+ # @return [String]
51
+ def context
52
+ @context ||= context_url
53
+ end
54
+
55
+ # Get property value
56
+ # @param property_name [to_s]
57
+ # @return [*]
58
+ def [](property_name)
59
+ if get_property(property_name).is_a?(::Frodo::Properties::Complex)
60
+ get_property(property_name)
61
+ else
62
+ get_property(property_name).value
63
+ end
64
+ end
65
+
66
+ # Set property value
67
+ # @param property_name [to_s]
68
+ # @param value [*]
69
+ def []=(property_name, value)
70
+ get_property(property_name).value = value
71
+ end
72
+
73
+ def get_property(property_name)
74
+ prop_name = property_name.to_s
75
+ # Property is lazy loaded
76
+ if properties_xml_value.has_key?(prop_name)
77
+ property = instantiate_property(prop_name, properties_xml_value[prop_name])
78
+ set_property(prop_name, property.dup)
79
+ properties_xml_value.delete(prop_name)
80
+ end
81
+
82
+ if properties.has_key? prop_name
83
+ properties[prop_name]
84
+ elsif navigation_properties.has_key? prop_name
85
+ navigation_properties[prop_name]
86
+ else
87
+ raise ArgumentError, "Unknown property: #{property_name}"
88
+ end
89
+ end
90
+
91
+ def property_names
92
+ [
93
+ @properties_xml_value.andand.keys,
94
+ @properties.andand.keys
95
+ ].compact.flatten
96
+ end
97
+
98
+ def navigation_property_names
99
+ navigation_properties.keys
100
+ end
101
+
102
+ def navigation_properties
103
+ @navigation_properties ||= links.keys.map do |nav_name|
104
+ [
105
+ nav_name,
106
+ Frodo::NavigationProperty::Proxy.new(self, nav_name)
107
+ ]
108
+ end.to_h
109
+ end
110
+
111
+ # Links to other Frodo entitites
112
+ # @return [Hash]
113
+ def links
114
+ @links ||= schema.navigation_properties[name].map do |nav_name, details|
115
+ [
116
+ nav_name,
117
+ { type: details.nav_type, href: "#{id}/#{nav_name}" }
118
+ ]
119
+ end.to_h
120
+ end
121
+
122
+ # Create Entity with provided properties and options.
123
+ # @param new_properties [Hash]
124
+ # @param options [Hash]
125
+ # @param [Frodo::Entity]
126
+ def self.with_properties(new_properties = {}, options = {})
127
+ entity = Frodo::Entity.new(options)
128
+ entity.instance_eval do
129
+ service.properties_for_entity(type).each do |property_name, instance|
130
+ set_property(property_name, instance)
131
+ end
132
+
133
+ new_properties.each do |property_name, property_value|
134
+ self[property_name] = property_value
135
+ end
136
+ end
137
+ entity
138
+ end
139
+
140
+ # Create Entity from JSON document with provided options.
141
+ # @param json [Hash|to_s]
142
+ # @param options [Hash]
143
+ # @return [Frodo::Entity]
144
+ def self.from_json(json, options = {})
145
+ return nil if json.nil?
146
+ json = JSON.parse(json.to_s) unless json.is_a?(Hash)
147
+ metadata = extract_metadata(json)
148
+ options.merge!(context: metadata['@odata.context'])
149
+ p json
150
+ p options
151
+ entity = with_properties(json, options)
152
+ process_metadata(entity, metadata)
153
+ entity
154
+ end
155
+
156
+ # Create Entity from XML document with provided options.
157
+ # @param xml_doc [Nokogiri::XML]
158
+ # @param options [Hash]
159
+ # @return [Frodo::Entity]
160
+ def self.from_xml(xml_doc, options = {})
161
+ return nil if xml_doc.nil?
162
+ entity = Frodo::Entity.new(options)
163
+ process_properties(entity, xml_doc)
164
+ process_links(entity, xml_doc)
165
+ entity
166
+ end
167
+
168
+ # Converts Entity to its XML representation.
169
+ # @return [String]
170
+ def to_xml
171
+ namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
172
+ builder = Nokogiri::XML::Builder.new do |xml|
173
+ xml.entry(namespaces) do
174
+ xml.category(term: type,
175
+ scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
176
+ xml.author { xml.name }
177
+
178
+ xml.content(type: 'application/xml') do
179
+ xml['metadata'].properties do
180
+ property_names.each do |name|
181
+ next if name == primary_key
182
+ get_property(name).to_xml(xml)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ builder.to_xml
189
+ end
190
+
191
+ # Converts Entity to its JSON representation.
192
+ # @return [String]
193
+ def to_json
194
+ # TODO: add @odata.context
195
+ to_hash.to_json
196
+ end
197
+
198
+ # Converts Entity to a hash.
199
+ # @return [Hash]
200
+ def to_hash
201
+ property_names.map do |name|
202
+ [name, get_property(name).json_value]
203
+ end.to_h
204
+ end
205
+
206
+ # Returns the canonical URL for this entity
207
+ # @return [String]
208
+ def id
209
+ @id ||= lambda {
210
+ entity_set = self.entity_set.andand.name
211
+ entity_set ||= context.split('#').last.split('/').first
212
+ "#{entity_set}(#{self[primary_key]})"
213
+ }.call
214
+ end
215
+
216
+ # Returns the primary key for the Entity.
217
+ # @return [String]
218
+ def primary_key
219
+ schema.primary_key_for(name)
220
+ end
221
+
222
+ def is_new?
223
+ self[primary_key].nil?
224
+ end
225
+
226
+ def any_errors?
227
+ !errors.empty?
228
+ end
229
+
230
+ def service
231
+ @service ||= Frodo::ServiceRegistry[service_name]
232
+ end
233
+
234
+ def schema
235
+ @schema ||= service.schemas[namespace]
236
+ end
237
+
238
+ private
239
+
240
+ def instantiate_property(property_name, value_xml)
241
+ prop_type = schema.get_property_type(name, property_name)
242
+ prop_type, value_type = prop_type.split(/\(|\)/)
243
+
244
+ if prop_type == 'Collection'
245
+ klass = ::Frodo::Properties::Collection
246
+ options = { value_type: value_type }
247
+ else
248
+ klass = ::Frodo::PropertyRegistry[prop_type]
249
+ options = {}
250
+ end
251
+
252
+ if klass.nil?
253
+ raise RuntimeError, "Unknown property type: #{prop_type}"
254
+ else
255
+ klass.from_xml(value_xml, options.merge(service: service))
256
+ end
257
+ end
258
+
259
+ def properties
260
+ @properties ||= {}
261
+ end
262
+
263
+ def properties_xml_value
264
+ @properties_xml_value ||= {}
265
+ end
266
+
267
+ # Computes the entity's canonical context URL
268
+ def context_url
269
+ "#{service.service_url}/$metadata##{entity_set.name}/$entity"
270
+ end
271
+
272
+ def set_property(name, property)
273
+ properties[name.to_s] = property
274
+ end
275
+
276
+ # Instantiating properties takes time, so we can lazy load properties by passing xml_value and lookup when needed
277
+ def set_property_lazy_load(name, xml_value )
278
+ properties_xml_value[name.to_s] = xml_value
279
+ end
280
+
281
+ def self.process_properties(entity, xml_doc)
282
+ entity.instance_eval do
283
+ unless instance_variable_get(:@context)
284
+ context = xml_doc.xpath('/entry').first.andand['context']
285
+ instance_variable_set(:@context, context)
286
+ end
287
+
288
+ xml_doc.xpath('./content/properties/*').each do |property_xml|
289
+ # Doing lazy loading here because instantiating each object takes a long time
290
+ set_property_lazy_load(property_xml.name, property_xml)
291
+ end
292
+ end
293
+ end
294
+
295
+ def self.process_links(entity, xml_doc)
296
+ entity.instance_eval do
297
+ new_links = instance_variable_get(:@links) || {}
298
+ schema.navigation_properties[name].each do |nav_name, details|
299
+ p nav_name
300
+ xml_doc.xpath("./link[@title='#{nav_name}']").each do |node|
301
+ next if node.attributes['type'].nil?
302
+ next unless node.attributes['type'].value =~ /^application\/atom\+xml;type=(feed|entry)$/i
303
+ link_type = node.attributes['type'].value =~ /type=entry$/i ? :entity : :collection
304
+ new_links[nav_name] = {
305
+ type: link_type,
306
+ href: node.attributes['href'].value
307
+ }
308
+ end
309
+ end
310
+ instance_variable_set(:@links, new_links)
311
+ end
312
+ end
313
+
314
+ def self.extract_metadata(json)
315
+ metadata = json.select { |key, val| key =~ /@odata/ }
316
+ json.delete_if { |key, val| key =~ /@odata/ }
317
+ metadata
318
+ end
319
+
320
+ def self.process_metadata(entity, metadata)
321
+ entity.instance_eval do
322
+ new_links = instance_variable_get(:@links) || {}
323
+ schema.navigation_properties[name].each do |nav_name, details|
324
+ href = metadata["#{nav_name}@odata.navigationLink"]
325
+ next if href.nil?
326
+ new_links[nav_name] = {
327
+ type: details.nav_type,
328
+ href: href
329
+ }
330
+ end
331
+ instance_variable_set(:@links, new_links) unless new_links.empty?
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,75 @@
1
+ module Frodo
2
+ #
3
+ class EntityContainer
4
+ # The EntityContainer's parent service
5
+ attr_reader :service
6
+ # The EntityContainer's metadata
7
+ attr_reader :metadata
8
+
9
+ # Creates a new EntityContainer
10
+ # @param service [Frodo::Service] The entity container's parent service
11
+ def initialize(service)
12
+ @metadata = service.metadata.xpath('//EntityContainer').first
13
+ @service = service
14
+ end
15
+
16
+ # The EntityContainer's surrounding Schema
17
+ # @return [Nokogiri::XML]
18
+ def schema
19
+ @schema ||= metadata.ancestors('Schema').first
20
+ end
21
+
22
+ # Returns the EntityContainer's namespace.
23
+ # @return [String]
24
+ def namespace
25
+ @namespace ||= schema.attributes['Namespace'].value
26
+ end
27
+
28
+ # Returns the EntityContainer's name.
29
+ # @return [String]
30
+ def name
31
+ @name ||= metadata.attributes['Name'].value
32
+ end
33
+
34
+ # Returns a hash of EntitySet names and their respective EntityTypes.
35
+ # @return [Hash<String, String>]
36
+ def entity_sets
37
+ @entity_sets ||= metadata.xpath('//EntitySet').map do |entity|
38
+ [
39
+ entity.attributes['Name'].value,
40
+ entity.attributes['EntityType'].value
41
+ ]
42
+ end.to_h
43
+ end
44
+
45
+ # Retrieves the EntitySet associated with a specific EntityType by name
46
+ #
47
+ # @param entity_set_name [to_s] the name of the EntitySet desired
48
+ # @return [Frodo::EntitySet] an Frodo::EntitySet to query
49
+ def [](entity_set_name)
50
+ xpath_query = "//EntitySet[@Name='#{entity_set_name}']"
51
+ entity_set_node = metadata.xpath(xpath_query).first
52
+ raise ArgumentError, "Unknown Entity Set: #{entity_set_name}" if entity_set_node.nil?
53
+ entity_type = entity_set_node.attributes['EntityType'].value
54
+ Frodo::EntitySet.new(
55
+ name: entity_set_name,
56
+ namespace: namespace,
57
+ type: entity_type,
58
+ service_name: service.name,
59
+ container: name
60
+ )
61
+ end
62
+
63
+ def singletons
64
+ # TODO return singletons exposed by this EntityContainer
65
+ end
66
+
67
+ def actions
68
+ # TODO return action imports exposed by this EntityContainer
69
+ end
70
+
71
+ def functions
72
+ # TODO return function imports exposed by this EntityContainer
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,131 @@
1
+ module Frodo
2
+ # This class represents a set of entities within an Frodo service. It is
3
+ # instantiated whenever an Frodo::Service is asked for an EntitySet via the
4
+ # Frodo::Service#[] method call. It also provides Enumerable behavior so that
5
+ # you can interact with the entities within a set in a very comfortable way.
6
+ #
7
+ # This class also implements a query interface for finding certain entities
8
+ # based on query criteria or limiting the result set returned by the set. This
9
+ # functionality is implemented through transparent proxy objects.
10
+ class EntitySet
11
+ include Enumerable
12
+
13
+ # The name of the EntitySet
14
+ attr_reader :name
15
+ # The Entity type for the EntitySet
16
+ attr_reader :type
17
+ # The Frodo::Service's namespace
18
+ attr_reader :namespace
19
+ # The Frodo::Service's identifiable name
20
+ attr_reader :service_name
21
+ # The EntitySet's container name
22
+ attr_reader :container
23
+
24
+ # Sets up the EntitySet to permit querying for the resources in the set.
25
+ #
26
+ # @param options [Hash] the options to setup the EntitySet
27
+ # @return [Frodo::EntitySet] an instance of the EntitySet
28
+ def initialize(options = {})
29
+ @name = options[:name]
30
+ @type = options[:type]
31
+ @namespace = options[:namespace]
32
+ @service_name = options[:service_name]
33
+ @container = options[:container]
34
+ end
35
+
36
+ # Provided for Enumerable functionality
37
+ #
38
+ # @param block [block] a block to evaluate
39
+ # @return [Frodo::Entity] each entity in turn from this set
40
+ # def each(&block)
41
+ # query.execute.each(&block)
42
+ # end
43
+
44
+ # # Return the first `n` Entities for the set.
45
+ # # If count is 1 it returns the single entity, otherwise its an array of entities
46
+ # # @return [Frodo::EntitySet]
47
+ # def first(count = 1)
48
+ # result = query.limit(count).execute
49
+ # count == 1 ? result.first : result.to_a
50
+ # end
51
+
52
+ # # Returns the number of entities within the set.
53
+ # # Not supported in Microsoft CRM2011
54
+ # # @return [Integer]
55
+ # def count
56
+ # query.count
57
+ # end
58
+
59
+ # Create a new Entity for this set with the given properties.
60
+ # @param properties [Hash] property name as key and it's initial value
61
+ # @return [Frodo::Entity]
62
+ def new_entity(properties = {})
63
+ Frodo::Entity.with_properties(properties, entity_options)
64
+ end
65
+
66
+ # Returns a query targetted at the current EntitySet.
67
+ # @param options [Hash] query options
68
+ # @return [Frodo::Query]
69
+ def query(options = {})
70
+ Frodo::Query.new(self, options)
71
+ end
72
+
73
+
74
+ def entity_primary_key()
75
+ new_entity.primary_key
76
+ end
77
+
78
+ # Find the Entity with the supplied key value.
79
+ # @param key [to_s] primary key to lookup
80
+ # @return [Frodo::Entity,nil]
81
+ # def [](key, options={})
82
+ # properties_to_expand = if options[:expand] == :all
83
+ # new_entity.navigation_property_names
84
+ # else
85
+ # [ options[:expand] ].compact.flatten
86
+ # end
87
+
88
+ # query.expand(*properties_to_expand).find(key)
89
+ # end
90
+
91
+ # Write supplied entity back to the service.
92
+ # TODO Test this more with CRM2011
93
+ # @param entity [Frodo::Entity] entity to save or update in the service
94
+ # @return [Frodo::Entity]
95
+ # def <<(entity)
96
+ # url_chunk, options = setup_entity_post_request(entity)
97
+
98
+ # result = execute_entity_post_request(options, url_chunk)
99
+ # if entity.is_new?
100
+ # doc = ::Nokogiri::XML(result.body).remove_namespaces!
101
+ # primary_key_node = doc.xpath("//content/properties/#{entity.primary_key}").first
102
+ # entity[entity.primary_key] = primary_key_node.content unless primary_key_node.nil?
103
+ # end
104
+
105
+ # unless result.status.to_s =~ /^2[0-9][0-9]$/
106
+ # entity.errors << ['could not commit entity']
107
+ # end
108
+
109
+ # entity
110
+ # end
111
+
112
+ # The Frodo::Service this EntitySet is associated with.
113
+ # @return [Frodo::Service]
114
+ # @api private
115
+ def service
116
+ @service ||= Frodo::ServiceRegistry[service_name]
117
+ end
118
+
119
+ # Options used for instantiating a new Frodo::Entity for this set.
120
+ # @return [Hash]
121
+ # @api private
122
+ def entity_options
123
+ {
124
+ service_name: service_name,
125
+ type: type,
126
+ entity_set: self
127
+ }
128
+ end
129
+
130
+ end
131
+ end