odata4 0.7.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 (114) 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 +120 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +23 -0
  11. data/README.md +287 -0
  12. data/Rakefile +7 -0
  13. data/TODO.md +55 -0
  14. data/lib/odata4.rb +37 -0
  15. data/lib/odata4/complex_type.rb +76 -0
  16. data/lib/odata4/complex_type/property.rb +114 -0
  17. data/lib/odata4/entity.rb +319 -0
  18. data/lib/odata4/entity_set.rb +162 -0
  19. data/lib/odata4/enum_type.rb +95 -0
  20. data/lib/odata4/enum_type/property.rb +62 -0
  21. data/lib/odata4/navigation_property.rb +29 -0
  22. data/lib/odata4/navigation_property/proxy.rb +76 -0
  23. data/lib/odata4/properties.rb +25 -0
  24. data/lib/odata4/properties/binary.rb +50 -0
  25. data/lib/odata4/properties/boolean.rb +37 -0
  26. data/lib/odata4/properties/date.rb +27 -0
  27. data/lib/odata4/properties/date_time.rb +83 -0
  28. data/lib/odata4/properties/date_time_offset.rb +17 -0
  29. data/lib/odata4/properties/decimal.rb +50 -0
  30. data/lib/odata4/properties/float.rb +67 -0
  31. data/lib/odata4/properties/geography.rb +13 -0
  32. data/lib/odata4/properties/geography/base.rb +162 -0
  33. data/lib/odata4/properties/geography/line_string.rb +33 -0
  34. data/lib/odata4/properties/geography/point.rb +31 -0
  35. data/lib/odata4/properties/geography/polygon.rb +38 -0
  36. data/lib/odata4/properties/guid.rb +17 -0
  37. data/lib/odata4/properties/integer.rb +107 -0
  38. data/lib/odata4/properties/number.rb +14 -0
  39. data/lib/odata4/properties/string.rb +72 -0
  40. data/lib/odata4/properties/time.rb +40 -0
  41. data/lib/odata4/properties/time_of_day.rb +27 -0
  42. data/lib/odata4/property.rb +118 -0
  43. data/lib/odata4/property_registry.rb +41 -0
  44. data/lib/odata4/query.rb +231 -0
  45. data/lib/odata4/query/criteria.rb +92 -0
  46. data/lib/odata4/query/criteria/comparison_operators.rb +49 -0
  47. data/lib/odata4/query/criteria/date_functions.rb +61 -0
  48. data/lib/odata4/query/criteria/geography_functions.rb +21 -0
  49. data/lib/odata4/query/criteria/lambda_operators.rb +27 -0
  50. data/lib/odata4/query/criteria/string_functions.rb +40 -0
  51. data/lib/odata4/query/in_batches.rb +58 -0
  52. data/lib/odata4/query/result.rb +84 -0
  53. data/lib/odata4/query/result/atom.rb +41 -0
  54. data/lib/odata4/query/result/json.rb +42 -0
  55. data/lib/odata4/railtie.rb +19 -0
  56. data/lib/odata4/service.rb +344 -0
  57. data/lib/odata4/service_registry.rb +52 -0
  58. data/lib/odata4/version.rb +3 -0
  59. data/odata4.gemspec +34 -0
  60. data/spec/fixtures/files/entity_to_xml.xml +17 -0
  61. data/spec/fixtures/files/metadata.xml +150 -0
  62. data/spec/fixtures/files/product_0.json +10 -0
  63. data/spec/fixtures/files/product_0.xml +28 -0
  64. data/spec/fixtures/files/products.json +106 -0
  65. data/spec/fixtures/files/products.xml +308 -0
  66. data/spec/fixtures/files/supplier_0.json +26 -0
  67. data/spec/fixtures/files/supplier_0.xml +32 -0
  68. data/spec/fixtures/vcr_cassettes/complex_type_specs.yml +127 -0
  69. data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1348 -0
  70. data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
  71. data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
  72. data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
  73. data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
  74. data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
  75. data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
  76. data/spec/fixtures/vcr_cassettes/query_specs.yml +663 -0
  77. data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
  78. data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
  79. data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +749 -0
  80. data/spec/odata4/complex_type_spec.rb +116 -0
  81. data/spec/odata4/entity/shared_examples.rb +82 -0
  82. data/spec/odata4/entity_set_spec.rb +168 -0
  83. data/spec/odata4/entity_spec.rb +151 -0
  84. data/spec/odata4/enum_type_spec.rb +134 -0
  85. data/spec/odata4/navigation_property/proxy_spec.rb +44 -0
  86. data/spec/odata4/navigation_property_spec.rb +55 -0
  87. data/spec/odata4/properties/binary_spec.rb +50 -0
  88. data/spec/odata4/properties/boolean_spec.rb +72 -0
  89. data/spec/odata4/properties/date_spec.rb +23 -0
  90. data/spec/odata4/properties/date_time_offset_spec.rb +30 -0
  91. data/spec/odata4/properties/date_time_spec.rb +23 -0
  92. data/spec/odata4/properties/decimal_spec.rb +24 -0
  93. data/spec/odata4/properties/float_spec.rb +45 -0
  94. data/spec/odata4/properties/geography/line_string_spec.rb +33 -0
  95. data/spec/odata4/properties/geography/point_spec.rb +29 -0
  96. data/spec/odata4/properties/geography/polygon_spec.rb +55 -0
  97. data/spec/odata4/properties/geography/shared_examples.rb +72 -0
  98. data/spec/odata4/properties/guid_spec.rb +17 -0
  99. data/spec/odata4/properties/integer_spec.rb +58 -0
  100. data/spec/odata4/properties/string_spec.rb +46 -0
  101. data/spec/odata4/properties/time_of_day_spec.rb +23 -0
  102. data/spec/odata4/properties/time_spec.rb +15 -0
  103. data/spec/odata4/property_registry_spec.rb +16 -0
  104. data/spec/odata4/property_spec.rb +32 -0
  105. data/spec/odata4/query/criteria_spec.rb +229 -0
  106. data/spec/odata4/query/result_spec.rb +53 -0
  107. data/spec/odata4/query_spec.rb +196 -0
  108. data/spec/odata4/service_registry_spec.rb +18 -0
  109. data/spec/odata4/service_spec.rb +80 -0
  110. data/spec/odata4/usage_example_spec.rb +176 -0
  111. data/spec/spec_helper.rb +32 -0
  112. data/spec/support/coverage.rb +2 -0
  113. data/spec/support/vcr.rb +9 -0
  114. metadata +380 -0
@@ -0,0 +1,162 @@
1
+ module OData4
2
+ # This class represents a set of entities within an OData4 service. It is
3
+ # instantiated whenever an OData4::Service is asked for an EntitySet via the
4
+ # OData4::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 OData4::Service's namespace
18
+ attr_reader :namespace
19
+ # The OData4::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 [OData4::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 [OData4::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 [OData4::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 [OData4::Entity]
62
+ def new_entity(properties = {})
63
+ OData4::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 [OData4::Query]
69
+ def query(options = {})
70
+ OData4::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 [OData4::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 [OData4::Entity] entity to save or update in the service
89
+ # @return [OData4::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 OData4::Service this EntitySet is associated with.
107
+ # @return [OData4::Service]
108
+ # @api private
109
+ def service
110
+ @service ||= OData4::ServiceRegistry[service_name]
111
+ end
112
+
113
+ # Options used for instantiating a new OData4::Entity for this set.
114
+ # @return [Hash]
115
+ # @api private
116
+ def entity_options
117
+ {
118
+ namespace: namespace,
119
+ service_name: service_name,
120
+ type: type,
121
+ entity_set: self
122
+ }
123
+ end
124
+
125
+ private
126
+
127
+ def execute_entity_post_request(options, url_chunk)
128
+ result = service.execute(url_chunk, options)
129
+ unless result.code.to_s =~ /^2[0-9][0-9]$/
130
+ service.logger.debug <<-EOS
131
+ [ODATA: #{service_name}]
132
+ An error was encountered committing your entity:
133
+
134
+ POSTed URL:
135
+ #{url_chunk}
136
+
137
+ POSTed Entity:
138
+ #{options[:body]}
139
+
140
+ Result Body:
141
+ #{result.body}
142
+ EOS
143
+ service.logger.info "[ODATA: #{service_name}] Unable to commit data to #{url_chunk}"
144
+ end
145
+ result
146
+ end
147
+
148
+ def setup_entity_post_request(entity)
149
+ primary_key = entity.get_property(entity.primary_key).url_value
150
+ chunk = entity.is_new? ? name : "#{name}(#{primary_key})"
151
+ options = {
152
+ method: :post,
153
+ body: entity.to_xml.gsub(/\n\s+/, ''),
154
+ headers: {
155
+ 'Accept' => 'application/atom+xml',
156
+ 'Content-Type' => 'application/atom+xml'
157
+ }
158
+ }
159
+ return chunk, options
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,95 @@
1
+ require 'odata4/enum_type/property'
2
+
3
+ module OData4
4
+ # Enumeration types are nominal types that represent a series of related values.
5
+ # Enumeration types expose these related values as members of the enumeration.
6
+ class EnumType
7
+ # Creates a new EnumType based on the supplied options.
8
+ # @param type_xml [Nokogiri::XML::Element]
9
+ # @param service [OData4::Service]
10
+ # @return [self]
11
+ def initialize(type_definition, service)
12
+ @type_definition = type_definition
13
+ @service = service
14
+ end
15
+
16
+ # The name of the EnumType
17
+ # @return [String]
18
+ def name
19
+ options['Name']
20
+ end
21
+
22
+ # Returns the namespaced type for the EnumType.
23
+ # @return [String]
24
+ def type
25
+ "#{namespace}.#{name}"
26
+ end
27
+
28
+ # Whether this EnumType supports setting multiple values.
29
+ # @return [Boolean]
30
+ def is_flags?
31
+ options['IsFlags'] == 'true'
32
+ end
33
+
34
+ # The underlying type of this EnumType.
35
+ # @return [String]
36
+ def underlying_type
37
+ options['UnderlyingType'] || 'Edm.Int32'
38
+ end
39
+
40
+ # Returns the namespace this EnumType belongs to.
41
+ # @return [String]
42
+ def namespace
43
+ @namespace ||= service.namespace
44
+ end
45
+
46
+ # Returns the members of this EnumType and their values.
47
+ # @return [Hash]
48
+ def members
49
+ @members ||= collect_members
50
+ end
51
+
52
+ # Returns the property class that implements this `EnumType`.
53
+ # @return [Class < OData4::EnumType::Property]
54
+ def property_class
55
+ @property_class ||= lambda { |type, members, is_flags|
56
+ klass = Class.new ::OData4::EnumType::Property
57
+ klass.send(:define_method, :type) { type }
58
+ klass.send(:define_method, :members) { members }
59
+ klass.send(:define_method, :is_flags?) { is_flags }
60
+ klass
61
+ }.call(type, members, is_flags?)
62
+ end
63
+
64
+ # Returns the value of the requested member.
65
+ # @param member_name [to_s]
66
+ # @return [*]
67
+ def [](member_name)
68
+ members.invert[member_name.to_s]
69
+ end
70
+
71
+ private
72
+
73
+ def service
74
+ @service
75
+ end
76
+
77
+ def type_definition
78
+ @type_definition
79
+ end
80
+
81
+ def options
82
+ @options = type_definition.attributes.map do |name, attr|
83
+ [name, attr.value]
84
+ end.to_h
85
+ end
86
+
87
+ def collect_members
88
+ Hash[type_definition.xpath('./Member').map.with_index do |member_xml, index|
89
+ member_name = member_xml.attributes['Name'].value
90
+ member_value = member_xml.attributes['Value'].andand.value.andand.to_i
91
+ [member_value || index, member_name]
92
+ end]
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,62 @@
1
+ module OData4
2
+ class EnumType
3
+ # Abstract base class for OData4 EnumTypes
4
+ # @see [OData4::EnumType]
5
+ class Property < OData4::Property
6
+ # Returns the property value, properly typecast
7
+ # @return [String, nil]
8
+ def value
9
+ if @value.nil? && allows_nil?
10
+ nil
11
+ else
12
+ @value
13
+ end
14
+ end
15
+
16
+ # Sets the property value
17
+ # @params new_value [String]
18
+ def value=(new_value)
19
+ parsed_value = validate(new_value)
20
+ @value = is_flags? ? parsed_value : parsed_value.first
21
+ end
22
+
23
+ # Value to be used in URLs.
24
+ # @return [String]
25
+ def url_value
26
+ "#{type}'#{@value}'"
27
+ end
28
+
29
+ private
30
+
31
+ def members
32
+ raise NotImplementedError, 'Subclass must override'
33
+ end
34
+
35
+ def validate(value)
36
+ return [] if value.nil? && allows_nil?
37
+ values = parse_value(value)
38
+
39
+ if values.length > 1 && !is_flags?
40
+ raise ArgumentError, 'Multiple values are not allowed for this property'
41
+ end
42
+
43
+ values.map do |value|
44
+ if members.keys.include?(value)
45
+ members[value]
46
+ elsif members.values.include?(value)
47
+ value
48
+ else
49
+ raise ArgumentError, "Property '#{name}': Value must be one of #{members.to_a}, but was: '#{value}'" if strict?
50
+ end
51
+ end.compact
52
+ end
53
+
54
+ def parse_value(value)
55
+ return nil if value.nil?
56
+ value.to_s.split(',').map(&:strip).map do |val|
57
+ val =~ /^[0-9]+$/ ? val.to_i : val
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ require 'odata4/navigation_property/proxy'
2
+
3
+ module OData4
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,76 @@
1
+ module OData4
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 ||= OData4::ServiceRegistry[entity.service_name]
31
+ end
32
+
33
+ def namespace
34
+ @namespace ||= service.namespace
35
+ end
36
+
37
+ def entity_type
38
+ @entity_type ||= entity.name
39
+ end
40
+
41
+ def link
42
+ entity.links[nav_name]
43
+ end
44
+
45
+ def nav_property
46
+ service.navigation_properties[entity_type][nav_name]
47
+ end
48
+
49
+ def fetch_result
50
+ raise "Invalid navigation link for #{nav_name}" unless link[:href]
51
+
52
+ options = {
53
+ type: nav_property.entity_type,
54
+ namespace: namespace,
55
+ service_name: entity.service_name
56
+ }
57
+ entity_set = Struct.new(:service, :entity_options)
58
+ .new(entity.service, options)
59
+
60
+ query = OData4::Query.new(entity_set)
61
+ begin
62
+ result = query.execute(link[:href])
63
+ rescue => ex
64
+ raise ex unless ex.message =~ /Not Found/
65
+ result = []
66
+ end
67
+
68
+ if nav_property.nav_type == :collection
69
+ result
70
+ else
71
+ result.first
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,25 @@
1
+ # Modules
2
+ require 'odata4/properties/number'
3
+
4
+ # Implementations
5
+ require 'odata4/properties/binary'
6
+ require 'odata4/properties/boolean'
7
+ require 'odata4/properties/date'
8
+ require 'odata4/properties/date_time'
9
+ require 'odata4/properties/date_time_offset'
10
+ require 'odata4/properties/decimal'
11
+ require 'odata4/properties/float'
12
+ require 'odata4/properties/geography'
13
+ require 'odata4/properties/guid'
14
+ require 'odata4/properties/integer'
15
+ require 'odata4/properties/string'
16
+ require 'odata4/properties/time'
17
+ require 'odata4/properties/time_of_day'
18
+
19
+ OData4::Properties.constants.each do |property_name|
20
+ klass = OData4::Properties.const_get(property_name)
21
+ if klass.is_a?(Class)
22
+ property = klass.new('test', nil)
23
+ OData4::PropertyRegistry.add(property.type, property.class)
24
+ end
25
+ end