frodata 0.9.1

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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +75 -0
  8. data/CHANGELOG.md +150 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +23 -0
  11. data/README.md +427 -0
  12. data/Rakefile +7 -0
  13. data/TODO.md +55 -0
  14. data/frodata.gemspec +34 -0
  15. data/lib/frodata.rb +36 -0
  16. data/lib/frodata/entity.rb +332 -0
  17. data/lib/frodata/entity_container.rb +75 -0
  18. data/lib/frodata/entity_set.rb +161 -0
  19. data/lib/frodata/errors.rb +68 -0
  20. data/lib/frodata/navigation_property.rb +29 -0
  21. data/lib/frodata/navigation_property/proxy.rb +80 -0
  22. data/lib/frodata/properties.rb +32 -0
  23. data/lib/frodata/properties/binary.rb +50 -0
  24. data/lib/frodata/properties/boolean.rb +37 -0
  25. data/lib/frodata/properties/collection.rb +50 -0
  26. data/lib/frodata/properties/complex.rb +114 -0
  27. data/lib/frodata/properties/date.rb +27 -0
  28. data/lib/frodata/properties/date_time.rb +83 -0
  29. data/lib/frodata/properties/date_time_offset.rb +17 -0
  30. data/lib/frodata/properties/decimal.rb +50 -0
  31. data/lib/frodata/properties/enum.rb +62 -0
  32. data/lib/frodata/properties/float.rb +67 -0
  33. data/lib/frodata/properties/geography.rb +13 -0
  34. data/lib/frodata/properties/geography/base.rb +162 -0
  35. data/lib/frodata/properties/geography/line_string.rb +33 -0
  36. data/lib/frodata/properties/geography/point.rb +31 -0
  37. data/lib/frodata/properties/geography/polygon.rb +38 -0
  38. data/lib/frodata/properties/guid.rb +17 -0
  39. data/lib/frodata/properties/integer.rb +107 -0
  40. data/lib/frodata/properties/number.rb +14 -0
  41. data/lib/frodata/properties/string.rb +72 -0
  42. data/lib/frodata/properties/time.rb +40 -0
  43. data/lib/frodata/properties/time_of_day.rb +27 -0
  44. data/lib/frodata/property.rb +139 -0
  45. data/lib/frodata/property_registry.rb +41 -0
  46. data/lib/frodata/query.rb +233 -0
  47. data/lib/frodata/query/criteria.rb +92 -0
  48. data/lib/frodata/query/criteria/comparison_operators.rb +49 -0
  49. data/lib/frodata/query/criteria/date_functions.rb +61 -0
  50. data/lib/frodata/query/criteria/geography_functions.rb +21 -0
  51. data/lib/frodata/query/criteria/lambda_operators.rb +27 -0
  52. data/lib/frodata/query/criteria/string_functions.rb +40 -0
  53. data/lib/frodata/query/in_batches.rb +58 -0
  54. data/lib/frodata/railtie.rb +19 -0
  55. data/lib/frodata/schema.rb +155 -0
  56. data/lib/frodata/schema/complex_type.rb +79 -0
  57. data/lib/frodata/schema/enum_type.rb +95 -0
  58. data/lib/frodata/service.rb +254 -0
  59. data/lib/frodata/service/request.rb +85 -0
  60. data/lib/frodata/service/response.rb +162 -0
  61. data/lib/frodata/service/response/atom.rb +40 -0
  62. data/lib/frodata/service/response/json.rb +41 -0
  63. data/lib/frodata/service/response/plain.rb +36 -0
  64. data/lib/frodata/service/response/xml.rb +40 -0
  65. data/lib/frodata/service_registry.rb +52 -0
  66. data/lib/frodata/version.rb +3 -0
  67. data/spec/fixtures/files/entity_to_xml.xml +17 -0
  68. data/spec/fixtures/files/error.xml +5 -0
  69. data/spec/fixtures/files/metadata.xml +150 -0
  70. data/spec/fixtures/files/product_0.json +10 -0
  71. data/spec/fixtures/files/product_0.xml +28 -0
  72. data/spec/fixtures/files/products.json +106 -0
  73. data/spec/fixtures/files/products.xml +308 -0
  74. data/spec/fixtures/files/supplier_0.json +26 -0
  75. data/spec/fixtures/files/supplier_0.xml +32 -0
  76. data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1635 -0
  77. data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
  78. data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
  79. data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
  80. data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
  81. data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
  82. data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
  83. data/spec/fixtures/vcr_cassettes/query_specs.yml +1060 -0
  84. data/spec/fixtures/vcr_cassettes/schema/complex_type_specs.yml +127 -0
  85. data/spec/fixtures/vcr_cassettes/service/request_specs.yml +193 -0
  86. data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
  87. data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
  88. data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +1330 -0
  89. data/spec/frodata/entity/shared_examples.rb +82 -0
  90. data/spec/frodata/entity_container_spec.rb +38 -0
  91. data/spec/frodata/entity_set_spec.rb +168 -0
  92. data/spec/frodata/entity_spec.rb +151 -0
  93. data/spec/frodata/errors_spec.rb +48 -0
  94. data/spec/frodata/navigation_property/proxy_spec.rb +44 -0
  95. data/spec/frodata/navigation_property_spec.rb +55 -0
  96. data/spec/frodata/properties/binary_spec.rb +50 -0
  97. data/spec/frodata/properties/boolean_spec.rb +72 -0
  98. data/spec/frodata/properties/collection_spec.rb +44 -0
  99. data/spec/frodata/properties/date_spec.rb +23 -0
  100. data/spec/frodata/properties/date_time_offset_spec.rb +30 -0
  101. data/spec/frodata/properties/date_time_spec.rb +23 -0
  102. data/spec/frodata/properties/decimal_spec.rb +51 -0
  103. data/spec/frodata/properties/float_spec.rb +45 -0
  104. data/spec/frodata/properties/geography/line_string_spec.rb +33 -0
  105. data/spec/frodata/properties/geography/point_spec.rb +29 -0
  106. data/spec/frodata/properties/geography/polygon_spec.rb +55 -0
  107. data/spec/frodata/properties/geography/shared_examples.rb +72 -0
  108. data/spec/frodata/properties/guid_spec.rb +17 -0
  109. data/spec/frodata/properties/integer_spec.rb +58 -0
  110. data/spec/frodata/properties/string_spec.rb +46 -0
  111. data/spec/frodata/properties/time_of_day_spec.rb +23 -0
  112. data/spec/frodata/properties/time_spec.rb +15 -0
  113. data/spec/frodata/property_registry_spec.rb +16 -0
  114. data/spec/frodata/property_spec.rb +71 -0
  115. data/spec/frodata/query/criteria_spec.rb +229 -0
  116. data/spec/frodata/query_spec.rb +199 -0
  117. data/spec/frodata/schema/complex_type_spec.rb +96 -0
  118. data/spec/frodata/schema/enum_type_spec.rb +112 -0
  119. data/spec/frodata/schema_spec.rb +97 -0
  120. data/spec/frodata/service/request_spec.rb +49 -0
  121. data/spec/frodata/service/response_spec.rb +85 -0
  122. data/spec/frodata/service_registry_spec.rb +18 -0
  123. data/spec/frodata/service_spec.rb +191 -0
  124. data/spec/frodata/usage_example_spec.rb +188 -0
  125. data/spec/spec_helper.rb +32 -0
  126. data/spec/support/coverage.rb +2 -0
  127. data/spec/support/vcr.rb +9 -0
  128. metadata +401 -0
@@ -0,0 +1,75 @@
1
+ module FrOData
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 [FrOData::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 [FrOData::EntitySet] an FrOData::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
+ FrOData::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,161 @@
1
+ module FrOData
2
+ # This class represents a set of entities within an FrOData service. It is
3
+ # instantiated whenever an FrOData::Service is asked for an EntitySet via the
4
+ # FrOData::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 FrOData::Service's namespace
18
+ attr_reader :namespace
19
+ # The FrOData::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 [FrOData::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 [FrOData::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 [FrOData::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 [FrOData::Entity]
62
+ def new_entity(properties = {})
63
+ FrOData::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 [FrOData::Query]
69
+ def query(options = {})
70
+ FrOData::Query.new(self, options)
71
+ end
72
+
73
+ # Find the Entity with the supplied key value.
74
+ # @param key [to_s] primary key to lookup
75
+ # @return [FrOData::Entity,nil]
76
+ def [](key, options={})
77
+ properties_to_expand = if options[:expand] == :all
78
+ new_entity.navigation_property_names
79
+ else
80
+ [ options[:expand] ].compact.flatten
81
+ end
82
+
83
+ query.expand(*properties_to_expand).find(key)
84
+ end
85
+
86
+ # Write supplied entity back to the service.
87
+ # TODO Test this more with CRM2011
88
+ # @param entity [FrOData::Entity] entity to save or update in the service
89
+ # @return [FrOData::Entity]
90
+ def <<(entity)
91
+ url_chunk, options = setup_entity_post_request(entity)
92
+ result = execute_entity_post_request(options, url_chunk)
93
+ if entity.is_new?
94
+ doc = ::Nokogiri::XML(result.body).remove_namespaces!
95
+ primary_key_node = doc.xpath("//content/properties/#{entity.primary_key}").first
96
+ entity[entity.primary_key] = primary_key_node.content unless primary_key_node.nil?
97
+ end
98
+
99
+ unless result.code.to_s =~ /^2[0-9][0-9]$/
100
+ entity.errors << ['could not commit entity']
101
+ end
102
+
103
+ entity
104
+ end
105
+
106
+ # The FrOData::Service this EntitySet is associated with.
107
+ # @return [FrOData::Service]
108
+ # @api private
109
+ def service
110
+ @service ||= FrOData::ServiceRegistry[service_name]
111
+ end
112
+
113
+ # Options used for instantiating a new FrOData::Entity for this set.
114
+ # @return [Hash]
115
+ # @api private
116
+ def entity_options
117
+ {
118
+ service_name: service_name,
119
+ type: type,
120
+ entity_set: self
121
+ }
122
+ end
123
+
124
+ private
125
+
126
+ def execute_entity_post_request(options, url_chunk)
127
+ result = service.execute(url_chunk, options)
128
+ unless result.code.to_s =~ /^2[0-9][0-9]$/
129
+ service.logger.debug <<-EOS
130
+ [ODATA: #{service_name}]
131
+ An error was encountered committing your entity:
132
+
133
+ POSTed URL:
134
+ #{url_chunk}
135
+
136
+ POSTed Entity:
137
+ #{options[:body]}
138
+
139
+ Result Body:
140
+ #{result.body}
141
+ EOS
142
+ service.logger.info "[ODATA: #{service_name}] Unable to commit data to #{url_chunk}"
143
+ end
144
+ result
145
+ end
146
+
147
+ def setup_entity_post_request(entity)
148
+ primary_key = entity.get_property(entity.primary_key).url_value
149
+ chunk = entity.is_new? ? name : "#{name}(#{primary_key})"
150
+ options = {
151
+ method: :post,
152
+ body: entity.to_xml.gsub(/\n\s+/, ''),
153
+ headers: {
154
+ 'Accept' => 'application/atom+xml',
155
+ 'Content-Type' => 'application/atom+xml'
156
+ }
157
+ }
158
+ return chunk, options
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,68 @@
1
+ module FrOData
2
+ # Base class for FrOData errors
3
+ class Error < StandardError
4
+ end
5
+
6
+ # Base class for network errors
7
+ class RequestError < Error
8
+ attr_reader :response
9
+
10
+ def initialize(response, message = nil)
11
+ @message = message
12
+ @response = response
13
+ end
14
+
15
+ def http_status
16
+ response.status
17
+ end
18
+
19
+ def message
20
+ [default_message, @message].compact.join(': ')
21
+ end
22
+
23
+ def default_message
24
+ nil
25
+ end
26
+ end
27
+
28
+ class ClientError < RequestError
29
+ end
30
+
31
+ class ServerError < RequestError
32
+ end
33
+
34
+ module Errors
35
+ ERROR_MAP = []
36
+
37
+ CLIENT_ERRORS = {
38
+ 400 => "Bad Request",
39
+ 401 => "Access Denied",
40
+ 403 => "Forbidden",
41
+ 404 => "Not Found",
42
+ 405 => "Method Not Allowed",
43
+ 406 => "Not Acceptable",
44
+ 413 => "Request Entity Too Large"
45
+ }
46
+
47
+ CLIENT_ERRORS.each do |code, message|
48
+ klass = Class.new(ClientError) do
49
+ send(:define_method, :default_message) { "#{code} #{message}" }
50
+ end
51
+ const_set(message.delete(' \-\''), klass)
52
+ ERROR_MAP[code] = klass
53
+ end
54
+
55
+ SERVER_ERRORS = {
56
+ 500 => "Internal Server Error",
57
+ 503 => "Service Unavailable"
58
+ }
59
+
60
+ SERVER_ERRORS.each do |code, message|
61
+ klass = Class.new(ServerError) do
62
+ send(:define_method, :default_message) { "#{code} #{message}" }
63
+ end
64
+ const_set(message.delete(' \-\''), klass)
65
+ ERROR_MAP[code] = klass
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,29 @@
1
+ require 'frodata/navigation_property/proxy'
2
+
3
+ module FrOData
4
+ class NavigationProperty
5
+ attr_reader :name, :type, :nullable, :partner
6
+
7
+ def initialize(options)
8
+ @name = options[:name] or raise ArgumentError, 'Name is required'
9
+ @type = options[:type] or raise ArgumentError, 'Type is required'
10
+ @nullable = options[:nullable] || true
11
+ @partner = options[:partner]
12
+ end
13
+
14
+ def nav_type
15
+ @nav_type ||= type =~ /^Collection/ ? :collection : :entity
16
+ end
17
+
18
+ def entity_type
19
+ @entity_type ||= type.split(/[()]/).last
20
+ end
21
+
22
+ def self.build(nav_property_xml)
23
+ options = nav_property_xml.attributes.map do |name, attr|
24
+ [name.downcase.to_sym, attr.value]
25
+ end.to_h
26
+ new(options)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ module FrOData
2
+ class NavigationProperty
3
+ class Proxy
4
+ def initialize(entity, nav_name)
5
+ @entity = entity
6
+ @nav_name = nav_name
7
+ end
8
+
9
+ def value=(value)
10
+ @value = value
11
+ end
12
+
13
+ def value
14
+ if link.nil?
15
+ if nav_property.nav_type == :collection
16
+ []
17
+ else
18
+ nil
19
+ end
20
+ else
21
+ @value ||= fetch_result
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :entity, :nav_name
28
+
29
+ def service
30
+ @service ||= FrOData::ServiceRegistry[entity.service_name]
31
+ end
32
+
33
+ def namespace
34
+ @namespace ||= service.namespace
35
+ end
36
+
37
+ def schema
38
+ @schema ||= service.schemas[namespace]
39
+ end
40
+
41
+ def entity_type
42
+ @entity_type ||= entity.name
43
+ end
44
+
45
+ def link
46
+ entity.links[nav_name]
47
+ end
48
+
49
+ def nav_property
50
+ schema.navigation_properties[entity_type][nav_name]
51
+ end
52
+
53
+ def fetch_result
54
+ raise "Invalid navigation link for #{nav_name}" unless link[:href]
55
+
56
+ options = {
57
+ type: nav_property.entity_type,
58
+ namespace: namespace,
59
+ service_name: entity.service_name
60
+ }
61
+ entity_set = Struct.new(:service, :entity_options)
62
+ .new(entity.service, options)
63
+
64
+ query = FrOData::Query.new(entity_set)
65
+ begin
66
+ result = query.execute(link[:href])
67
+ rescue => ex
68
+ raise ex unless ex.message =~ /Not Found/
69
+ result = []
70
+ end
71
+
72
+ if nav_property.nav_type == :collection
73
+ result
74
+ else
75
+ result.first
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ # Modules
2
+ require 'frodata/properties/number'
3
+
4
+ # Implementations
5
+ require 'frodata/properties/binary'
6
+ require 'frodata/properties/boolean'
7
+ require 'frodata/properties/collection'
8
+ require 'frodata/properties/complex'
9
+ require 'frodata/properties/date'
10
+ require 'frodata/properties/date_time'
11
+ require 'frodata/properties/date_time_offset'
12
+ require 'frodata/properties/decimal'
13
+ require 'frodata/properties/enum'
14
+ require 'frodata/properties/float'
15
+ require 'frodata/properties/geography'
16
+ require 'frodata/properties/guid'
17
+ require 'frodata/properties/integer'
18
+ require 'frodata/properties/string'
19
+ require 'frodata/properties/time'
20
+ require 'frodata/properties/time_of_day'
21
+
22
+ FrOData::Properties.constants.each do |property_name|
23
+ klass = FrOData::Properties.const_get(property_name)
24
+ if klass.is_a?(Class)
25
+ begin
26
+ property = klass.new('test', nil)
27
+ FrOData::PropertyRegistry.add(property.type, property.class)
28
+ rescue NotImplementedError
29
+ # Abstract type classes cannot be instantiated, ignore
30
+ end
31
+ end
32
+ end