odata4 0.7.0 → 0.8.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.
@@ -46,7 +46,7 @@ module OData4
46
46
  elsif members.values.include?(value)
47
47
  value
48
48
  else
49
- raise ArgumentError, "Property '#{name}': Value must be one of #{members.to_a}, but was: '#{value}'" if strict?
49
+ validation_error "Value must be one of #{members.to_a}, but was: '#{value}'"
50
50
  end
51
51
  end.compact
52
52
  end
@@ -34,6 +34,10 @@ module OData4
34
34
  @namespace ||= service.namespace
35
35
  end
36
36
 
37
+ def schema
38
+ @schema ||= service.schemas[namespace]
39
+ end
40
+
37
41
  def entity_type
38
42
  @entity_type ||= entity.name
39
43
  end
@@ -43,7 +47,7 @@ module OData4
43
47
  end
44
48
 
45
49
  def nav_property
46
- service.navigation_properties[entity_type][nav_name]
50
+ schema.navigation_properties[entity_type][nav_name]
47
51
  end
48
52
 
49
53
  def fetch_result
@@ -42,9 +42,9 @@ module OData4
42
42
 
43
43
  def validate(value)
44
44
  unless [0,1,'0','1',true,false].include?(value)
45
- raise ArgumentError, 'Value is outside accepted range: 0 or 1'
45
+ validation_error 'Value is outside accepted range: 0 or 1'
46
46
  end
47
47
  end
48
48
  end
49
49
  end
50
- end
50
+ end
@@ -29,7 +29,7 @@ module OData4
29
29
  def validate(value)
30
30
  return if value.nil? && allows_nil?
31
31
  unless [0,1,'0','1','true','false',true,false].include?(value)
32
- raise ArgumentError, 'Value is outside accepted range: true or false'
32
+ validation_error 'Value is outside accepted range: true or false'
33
33
  end
34
34
  end
35
35
  end
@@ -65,7 +65,7 @@ module OData4
65
65
  return if value.is_a?(date_class)
66
66
  date_class.parse(value)
67
67
  rescue
68
- raise ArgumentError, "Value '#{value}' is not a date time format that can be parsed"
68
+ validation_error "Value '#{value}' is not a date time format that can be parsed"
69
69
  end
70
70
  end
71
71
 
@@ -27,14 +27,14 @@ module OData4
27
27
  # Value to be used in URLs.
28
28
  # @return [String]
29
29
  def url_value
30
- "#{value.to_f}M"
30
+ "#{value.to_f}"
31
31
  end
32
32
 
33
33
  private
34
34
 
35
35
  def validate(value)
36
36
  if value > max_value || value < min_value || value.precs.first > 29
37
- raise ArgumentError, "Value is outside accepted range: #{min_value} to #{max_value}, or has more than 29 significant digits"
37
+ validation_error "Value is outside accepted range: #{min_value} to #{max_value}, or has more than 29 significant digits"
38
38
  end
39
39
  end
40
40
 
@@ -6,9 +6,9 @@ module OData4
6
6
 
7
7
  def validate(value)
8
8
  if value > max_value || value < min_value
9
- raise ::ArgumentError, "Value is outside accepted range: #{min_value} to #{max_value}"
9
+ validation_error "Value is outside accepted range: #{min_value} to #{max_value}"
10
10
  end
11
11
  end
12
12
  end
13
13
  end
14
- end
14
+ end
@@ -56,7 +56,7 @@ module OData4
56
56
 
57
57
  def validate(new_value)
58
58
  if new_value.nil? && !allows_nil?
59
- raise ArgumentError, 'This property does not allow for nil values to be set'
59
+ validation_error 'This property does not allow for nil values to be set'
60
60
  end
61
61
  end
62
62
 
@@ -69,4 +69,4 @@ module OData4
69
69
  end
70
70
  end
71
71
  end
72
- end
72
+ end
@@ -28,7 +28,7 @@ module OData4
28
28
 
29
29
  def validate(value)
30
30
  unless value.is_a?(::Time)
31
- raise ArgumentError, 'Value is not a time object'
31
+ validation_error 'Value is not a time object'
32
32
  end
33
33
  end
34
34
 
@@ -8,6 +8,8 @@ module OData4
8
8
  attr_reader :name
9
9
  # The property's value
10
10
  attr_accessor :value
11
+ # The property's options
12
+ attr_reader :options
11
13
 
12
14
  # Default intialization for a property with a name, value and options.
13
15
  # @param name [to_s]
@@ -44,13 +46,19 @@ module OData4
44
46
  # (Default=false)
45
47
  # @return [Boolean]
46
48
  def strict?
47
- options[:strict]
49
+ if options.key? :strict
50
+ options[:strict]
51
+ elsif service
52
+ service.options[:strict]
53
+ else
54
+ true
55
+ end
48
56
  end
49
57
 
50
58
  # The configured concurrency mode for the property.
51
59
  # @return [String]
52
60
  def concurrency_mode
53
- @concurrecy_mode ||= options[:concurrency_mode]
61
+ @concurrency_mode ||= options[:concurrency_mode]
54
62
  end
55
63
 
56
64
  # Value to be used in XML.
@@ -76,7 +84,7 @@ module OData4
76
84
  # @param xml_builder [Nokogiri::XML::Builder]
77
85
  def to_xml(xml_builder)
78
86
  attributes = {
79
- 'metadata:type' => type,
87
+ 'metadata:type' => type,
80
88
  }
81
89
 
82
90
  if value.nil?
@@ -101,18 +109,31 @@ module OData4
101
109
  new(property_xml.name, content, options)
102
110
  end
103
111
 
104
- private
112
+ protected
105
113
 
106
114
  def default_options
107
115
  {
108
116
  allows_nil: true,
109
- concurrency_mode: :none,
110
- strict: false
117
+ concurrency_mode: :none
111
118
  }
112
119
  end
113
120
 
114
- def options
115
- @options
121
+ def service
122
+ options[:service]
123
+ end
124
+
125
+ def logger
126
+ # Use a dummy logger if service is not available (-> unit tests)
127
+ @logger ||= service.andand.logger || Logger.new('/dev/null')
128
+ end
129
+
130
+ def validation_error(message)
131
+ if strict?
132
+ raise ArgumentError, "#{name}: #{message}"
133
+ else
134
+ logger.warn "#{name}: #{message}"
135
+ nil
136
+ end
116
137
  end
117
138
  end
118
139
  end
@@ -0,0 +1,146 @@
1
+ module OData4
2
+ class Schema
3
+ # The schema's parent service
4
+ attr_reader :service
5
+ # The schema's metadata (i.e its XML definition)
6
+ attr_reader :metadata
7
+
8
+ # Creates a new schema.
9
+ #
10
+ # @param schema_definition [Nokogiri::XML] The schema's XML definition
11
+ # @param service [OData4::Service] The schema's parent service
12
+ def initialize(schema_definition, service)
13
+ @metadata = schema_definition
14
+ @service = service
15
+ end
16
+
17
+ # Returns the schema's `Namespace` attribute (mandatory).
18
+ # @return [String]
19
+ def namespace
20
+ @namespace ||= metadata.attributes['Namespace'].value
21
+ end
22
+
23
+ # Returns a list of actions defined by the schema.
24
+ # @return [Array<String>]
25
+ def actions
26
+ @actions ||= metadata.xpath('//Action').map do |action|
27
+ action.attributes['Name'].value
28
+ end
29
+ end
30
+
31
+ # Returns a list of entities defined by the schema.
32
+ # @return [Array<String>]
33
+ def entity_types
34
+ @entity_types ||= metadata.xpath('//EntityType').map do |entity|
35
+ entity.attributes['Name'].value
36
+ end
37
+ end
38
+
39
+ # Returns a list of `ComplexType`s defined by the schema.
40
+ # @return [Hash<String, OData4::ComplexType>]
41
+ def complex_types
42
+ @complex_types ||= metadata.xpath('//ComplexType').map do |entity|
43
+ [
44
+ entity.attributes['Name'].value,
45
+ ::OData4::ComplexType.new(entity, self)
46
+ ]
47
+ end.to_h
48
+ end
49
+
50
+ # Returns a list of EnumTypes defined by the schema.
51
+ # @return [Hash<String, OData4::EnumType>]
52
+ def enum_types
53
+ @enum_types ||= metadata.xpath('//EnumType').map do |entity|
54
+ [
55
+ entity.attributes['Name'].value,
56
+ ::OData4::EnumType.new(entity, self)
57
+ ]
58
+ end.to_h
59
+ end
60
+
61
+ # Returns a list of functions defined by the schema.
62
+ # @return [Array<String>]
63
+ def functions
64
+ @functions ||= metadata.xpath('//Function').map do |function|
65
+ function.attributes['Name'].value
66
+ end
67
+ end
68
+
69
+ # Returns a list of type definitions defined by the schema.
70
+ # @return [Array<String>]
71
+ def type_definitions
72
+ @typedefs ||= metadata.xpath('//TypeDefinition').map do |typedef|
73
+ typedef.attributes['Name'].value
74
+ end
75
+ end
76
+
77
+ # Returns a hash for finding an association through an entity type's defined
78
+ # NavigationProperty elements.
79
+ # @return [Hash<Hash<OData4::NavigationProperty>>]
80
+ def navigation_properties
81
+ @navigation_properties ||= metadata.xpath('//EntityType').map do |entity_type_def|
82
+ [
83
+ entity_type_def.attributes['Name'].value,
84
+ entity_type_def.xpath('./NavigationProperty').map do |nav_property_def|
85
+ [
86
+ nav_property_def.attributes['Name'].value,
87
+ ::OData4::NavigationProperty.build(nav_property_def)
88
+ ]
89
+ end.to_h
90
+ ]
91
+ end.to_h
92
+ end
93
+
94
+ # Get the property type for an entity from metadata.
95
+ #
96
+ # @param entity_name [to_s] the name of the relevant entity
97
+ # @param property_name [to_s] the property name needed
98
+ # @return [String] the name of the property's type
99
+ def get_property_type(entity_name, property_name)
100
+ metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@Name='#{property_name}']").first.attributes['Type'].value
101
+ end
102
+
103
+ # Get the primary key for the supplied Entity.
104
+ #
105
+ # @param entity_name [to_s]
106
+ # @return [String]
107
+ def primary_key_for(entity_name)
108
+ metadata.xpath("//EntityType[@Name='#{entity_name}']/Key/PropertyRef").first.attributes['Name'].value
109
+ end
110
+
111
+ # Get the list of properties and their various options for the supplied
112
+ # Entity name.
113
+ # @param entity_name [to_s]
114
+ # @return [Hash]
115
+ # @api private
116
+ def properties_for_entity(entity_name)
117
+ type_definition = metadata.xpath("//EntityType[@Name='#{entity_name}']").first
118
+ raise ArgumentError, "Unknown EntityType: #{entity_name}" if type_definition.nil?
119
+ properties_to_return = {}
120
+ type_definition.xpath('./Property').each do |property_xml|
121
+ property_name, property = process_property_from_xml(property_xml)
122
+ properties_to_return[property_name] = property
123
+ end
124
+ properties_to_return
125
+ end
126
+
127
+ private
128
+
129
+ def process_property_from_xml(property_xml)
130
+ property_name = property_xml.attributes['Name'].value
131
+ value_type = property_xml.attributes['Type'].value
132
+ property_options = { service: service }
133
+
134
+ klass = ::OData4::PropertyRegistry[value_type]
135
+
136
+ if klass.nil?
137
+ raise RuntimeError, "Unknown property type: #{value_type}"
138
+ else
139
+ property_options[:allows_nil] = false if property_xml.attributes['Nullable'] == 'false'
140
+ property = klass.new(property_name, nil, property_options)
141
+ end
142
+
143
+ return [property_name, property]
144
+ end
145
+ end
146
+ end
@@ -52,64 +52,78 @@ module OData4
52
52
  "#{service_url}/$metadata"
53
53
  end
54
54
 
55
- # Returns a list of entities exposed by the service
56
- def entity_types
57
- @entity_types ||= metadata.xpath('//EntityType').collect {|entity| entity.attributes['Name'].value}
55
+ # Returns the service's metadata definition.
56
+ # @return [Nokogiri::XML]
57
+ def metadata
58
+ @metadata ||= lambda { read_metadata }.call
58
59
  end
59
60
 
60
- # Returns a hash of EntitySet names keyed to their respective EntityType name
61
- def entity_sets
62
- @entity_sets ||= metadata.xpath('//EntityContainer/EntitySet').collect {|entity|
61
+ # Returns all of the service's schemas.
62
+ # @return Hash<String, OData4::Schema>
63
+ def schemas
64
+ @schemas ||= metadata.xpath('//Schema').map do |schema_xml|
63
65
  [
64
- entity.attributes['EntityType'].value.gsub("#{namespace}.", ''),
65
- entity.attributes['Name'].value
66
+ schema_xml.attributes['Namespace'].value,
67
+ Schema.new(schema_xml, self)
66
68
  ]
67
- }.to_h
69
+ end.to_h
68
70
  end
69
71
 
70
- # Returns a list of ComplexTypes used by the service
71
- # @return [Hash<String, OData4::ComplexType>]
72
- def complex_types
73
- @complex_types ||= metadata.xpath('//ComplexType').map do |entity|
74
- [
75
- entity.attributes['Name'].value,
76
- ::OData4::ComplexType.new(entity, self)
77
- ]
78
- end.to_h
72
+ # Returns the service's EntityContainer (singleton)
73
+ # @return OData4::EntityContainer
74
+ def entity_container
75
+ @entity_container ||= EntityContainer.new(self)
79
76
  end
80
77
 
81
- # Returns a list of EnumTypes used by the service
82
- # @return [Hash<String, OData4::EnumType>]
83
- def enum_types
84
- @enum_types ||= metadata.xpath('//EnumType').map do |entity|
85
- [
86
- entity.attributes['Name'].value,
87
- ::OData4::EnumType.new(entity, self)
88
- ]
89
- end.to_h
78
+ # Returns a hash of EntitySet names and their respective EntityType names
79
+ # @return Hash<String, String>
80
+ def entity_sets
81
+ entity_container.entity_sets
90
82
  end
91
83
 
92
- # Returns a hash for finding an association through an entity type's defined
93
- # NavigationProperty elements.
94
- # @return [Hash<Hash<OData4::Association>>]
95
- def navigation_properties
96
- @navigation_properties ||= metadata.xpath('//EntityType').collect do |entity_type_def|
97
- entity_type_name = entity_type_def.attributes['Name'].value
98
- [
99
- entity_type_name,
100
- entity_type_def.xpath('./NavigationProperty').collect do |nav_property_def|
101
- [
102
- nav_property_def.attributes['Name'].value,
103
- ::OData4::NavigationProperty.build(nav_property_def)
104
- ]
105
- end.to_h
106
- ]
107
- end.to_h
84
+ # Retrieves the EntitySet associated with a specific EntityType by name
85
+ #
86
+ # @param entity_set_name [to_s] the name of the EntitySet desired
87
+ # @return [OData4::EntitySet] an OData4::EntitySet to query
88
+ def [](entity_set_name)
89
+ entity_container[entity_set_name]
108
90
  end
109
91
 
110
- # Returns the namespace defined on the service's schema
92
+ # Returns the default namespace, that is, the namespace of the schema
93
+ # that contains the service's EntityContainer.
94
+ # @return [String]
111
95
  def namespace
112
- @namespace ||= metadata.xpath('//Schema').first.attributes['Namespace'].value
96
+ entity_container.namespace
97
+ end
98
+
99
+ # Returns a list of `EntityType`s exposed by the service
100
+ # @return Array<String>
101
+ def entity_types
102
+ @entity_types ||= schemas.map do |namespace, schema|
103
+ schema.entity_types.map do |entity_type|
104
+ "#{namespace}.#{entity_type}"
105
+ end
106
+ end.flatten
107
+ end
108
+
109
+ # Returns a list of `ComplexType`s used by the service.
110
+ # @return [Hash<String, OData4::ComplexType>]
111
+ def complex_types
112
+ @complex_types ||= schemas.map do |namespace, schema|
113
+ schema.complex_types.map do |name, complex_type|
114
+ [ "#{namespace}.#{name}", complex_type ]
115
+ end.to_h
116
+ end.reduce({}, :merge)
117
+ end
118
+
119
+ # Returns a list of `EnumType`s used by the service
120
+ # @return [Hash<String, OData4::EnumType>]
121
+ def enum_types
122
+ @enum_types ||= schemas.map do |namespace, schema|
123
+ schema.enum_types.map do |name, enum_type|
124
+ [ "#{namespace}.#{name}", enum_type ]
125
+ end.to_h
126
+ end.reduce({}, :merge)
113
127
  end
114
128
 
115
129
  # Returns a more compact inspection of the service object
@@ -117,23 +131,6 @@ module OData4
117
131
  "#<#{self.class.name}:#{self.object_id} name='#{name}' service_url='#{self.service_url}'>"
118
132
  end
119
133
 
120
- # Retrieves the EntitySet associated with a specific EntityType by name
121
- #
122
- # @param entity_set_name [to_s] the name of the EntitySet desired
123
- # @return [OData4::EntitySet] an OData4::EntitySet to query
124
- def [](entity_set_name)
125
- xpath_query = "//EntityContainer/EntitySet[@Name='#{entity_set_name}']"
126
- entity_set_node = metadata.xpath(xpath_query).first
127
- raise ArgumentError, "Unknown Entity Set: #{entity_set_name}" if entity_set_node.nil?
128
- container_name = entity_set_node.parent.attributes['Name'].value
129
- entity_type_name = entity_set_node.attributes['EntityType'].value.gsub(/#{namespace}\./, '')
130
- OData4::EntitySet.new(name: entity_set_name,
131
- namespace: namespace,
132
- type: entity_type_name.to_s,
133
- service_name: name,
134
- container: container_name)
135
- end
136
-
137
134
  # Execute a request against the service
138
135
  #
139
136
  # @param url_chunk [to_s] string to append to service url
@@ -189,19 +186,23 @@ module OData4
189
186
 
190
187
  # Get the property type for an entity from metadata.
191
188
  #
192
- # @param entity_name [to_s] the name of the relevant entity
189
+ # @param entity_name [to_s] the fully qualified entity name
193
190
  # @param property_name [to_s] the property name needed
194
191
  # @return [String] the name of the property's type
195
192
  def get_property_type(entity_name, property_name)
196
- metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@Name='#{property_name}']").first.attributes['Type'].value
193
+ namespace, _, entity_name = entity_name.rpartition('.')
194
+ raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
195
+ schemas[namespace].get_property_type(entity_name, property_name)
197
196
  end
198
197
 
199
198
  # Get the primary key for the supplied Entity.
200
199
  #
201
- # @param entity_name [to_s]
200
+ # @param entity_name [to_s] The fully qualified entity name
202
201
  # @return [String]
203
202
  def primary_key_for(entity_name)
204
- metadata.xpath("//EntityType[@Name='#{entity_name}']/Key/PropertyRef").first.attributes['Name'].value
203
+ namespace, _, entity_name = entity_name.rpartition('.')
204
+ raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
205
+ schemas[namespace].primary_key_for(entity_name)
205
206
  end
206
207
 
207
208
  # Get the list of properties and their various options for the supplied
@@ -210,14 +211,9 @@ module OData4
210
211
  # @return [Hash]
211
212
  # @api private
212
213
  def properties_for_entity(entity_name)
213
- type_definition = metadata.xpath("//EntityType[@Name='#{entity_name}']").first
214
- raise ArgumentError, "Unknown EntityType: #{entity_name}" if type_definition.nil?
215
- properties_to_return = {}
216
- type_definition.xpath('./Property').each do |property_xml|
217
- property_name, property = process_property_from_xml(property_xml)
218
- properties_to_return[property_name] = property
219
- end
220
- properties_to_return
214
+ namespace, _, entity_name = entity_name.rpartition('.')
215
+ raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
216
+ schemas[namespace].properties_for_entity(entity_name)
221
217
  end
222
218
 
223
219
  # Returns the log level set via initial options, or the
@@ -262,7 +258,8 @@ module OData4
262
258
  typhoeus: {
263
259
  headers: { 'OData4-Version' => '4.0' },
264
260
  timeout: HTTP_TIMEOUT
265
- }
261
+ },
262
+ strict: true # strict property validation
266
263
  }
267
264
  end
268
265
 
@@ -276,10 +273,6 @@ module OData4
276
273
  end
277
274
  end
278
275
 
279
- def metadata
280
- @metadata ||= lambda { read_metadata }.call
281
- end
282
-
283
276
  def read_metadata
284
277
  response = nil
285
278
  # From file, good for debugging
@@ -314,30 +307,13 @@ module OData4
314
307
  OData4::Query::Result.new(nil, response).error_message
315
308
  end
316
309
 
317
- def process_property_from_xml(property_xml)
318
- property_name = property_xml.attributes['Name'].value
319
- value_type = property_xml.attributes['Type'].value
320
- property_options = {}
321
-
322
- klass = ::OData4::PropertyRegistry[value_type]
323
-
324
- if klass.nil?
325
- raise RuntimeError, "Unknown property type: #{value_type}"
326
- else
327
- property_options[:allows_nil] = false if property_xml.attributes['Nullable'] == 'false'
328
- property = klass.new(property_name, nil, property_options)
329
- end
330
-
331
- return [property_name, property]
332
- end
333
-
334
310
  def register_custom_types
335
311
  complex_types.each do |name, type|
336
- ::OData4::PropertyRegistry.add(type.type, type.property_class)
312
+ ::OData4::PropertyRegistry.add(name, type.property_class)
337
313
  end
338
314
 
339
315
  enum_types.each do |name, type|
340
- ::OData4::PropertyRegistry.add(type.type, type.property_class)
316
+ ::OData4::PropertyRegistry.add(name, type.property_class)
341
317
  end
342
318
  end
343
319
  end