odata4 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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