lib/ruby_odata/service.rb in ruby_odata-0.1.3 vs lib/ruby_odata/service.rb in ruby_odata-0.1.4

- old
+ new

@@ -1,9 +1,9 @@ module OData # The main service class, also known as a *Context* class Service - attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports + attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports, :response # Creates a new instance of the Service class # # @param [String] service_uri the root URI of the OData service # @param [Hash] options the options to pass to the service # @option options [String] :username for http basic auth @@ -95,13 +95,13 @@ end # Performs query operations (Read) against the server. # Typically this returns an array of record instances, except in the case of count queries def execute - result = RestClient::Resource.new(build_query_uri, @rest_options).get - return Integer(result) if result =~ /^\d+$/ - handle_collection_result(result) + @response = RestClient::Resource.new(build_query_uri, @rest_options).get + return Integer(@response) if @response =~ /^\d+$/ + handle_collection_result(@response) end # Overridden to identify methods handled by method_missing def respond_to?(method) if @collections.include?(method.to_s) @@ -174,10 +174,11 @@ end @rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] } @rest_options.merge!(options[:rest_options] || {}) @additional_params = options[:additional_params] || {} @namespace = options[:namespace] + @json_type = options[:json_type] || :json end def default_instance_vars! @collections = {} @function_imports = {} @@ -310,11 +311,11 @@ results end # Handles errors from the OData service def handle_exception(e) - raise e unless e.response + raise e unless defined? e.response code = e.http_code error = Nokogiri::XML(e.response) message = error.xpath("m:error/m:message", @ds_namespaces).first.content @@ -373,26 +374,22 @@ klass_name = title.content.to_s end return nil if klass_name.nil? - # If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL - # have properties that are ancestors of m:inline. Check if there is an m:inline child to determine the xpath query to use - has_inline = entry.xpath(".//m:inline", @ds_namespaces).any? - properties_xpath = has_inline ? ".//m:properties[not(ancestor::m:inline)]/*" : ".//m:properties/*" - properties = entry.xpath(properties_xpath, @ds_namespaces) + properties = entry.xpath("./atom:content/m:properties/*", @ds_namespaces) klass = @classes[qualify_class_name(klass_name)].new # Fill metadata meta_id = entry.xpath("./atom:id", @ds_namespaces)[0].content klass.send :__metadata=, { :uri => meta_id } # Fill properties for prop in properties prop_name = prop.name - klass.send "#{prop_name}=", parse_value(prop) + klass.send "#{prop_name}=", parse_value_xml(prop) end # Fill properties represented outside of the properties collection @class_metadata[qualify_class_name(klass_name)].select { |k,v| v.fc_keep_in_content == false }.each do |k, meta| if meta.fc_target_path == "SyndicationTitle" @@ -405,19 +402,18 @@ end inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces) for link in inline_links - inline_entries = link.xpath(".//atom:entry", @ds_namespaces) - # TODO: Use the metadata's associations to determine the multiplicity instead of this "hack" property_name = link.attributes['title'].to_s - if inline_entries.length == 1 && singular?(property_name) - inline_klass = build_inline_class(klass, inline_entries[0], property_name) + if singular?(property_name) + inline_entry = link.xpath("./m:inline/atom:entry", @ds_namespaces).first + inline_klass = build_inline_class(klass, inline_entry, property_name) klass.send "#{property_name}=", inline_klass else - inline_classes = [] + inline_classes, inline_entries = [], link.xpath("./m:inline/atom:feed/atom:entry", @ds_namespaces) for inline_entry in inline_entries # Build the class inline_klass = entry_to_class(inline_entry) # Add the property to the temp collection @@ -436,11 +432,11 @@ # Tests for and extracts the next href of a partial def extract_partial(doc) next_links = doc.xpath('//atom:link[@rel="next"]', @ds_namespaces) @has_partial = next_links.any? if @has_partial - uri = Addressable::URI.parse(next_links[0]['href']) + uri = Addressable::URI.parse(next_links[0]['href']) uri.query_values = uri.query_values.merge @additional_params unless @additional_params.empty? @next_uri = uri.to_s end end @@ -535,25 +531,25 @@ def single_save(operation) if operation.kind == "Add" save_uri = build_save_uri(operation) json_klass = operation.klass.to_json(:type => :add) - post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json} + post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type} return build_classes_from_result(post_result) elsif operation.kind == "Update" update_uri = build_resource_uri(operation) json_klass = operation.klass.to_json - update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => :json} + update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => @json_type} return (update_result.code == 204) elsif operation.kind == "Delete" delete_uri = build_resource_uri(operation) delete_result = RestClient::Resource.new(delete_uri, @rest_options).delete return (delete_result.code == 204) elsif operation.kind == "AddLink" save_uri = build_add_link_uri(operation) json_klass = operation.child_klass.to_json(:type => :link) - post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json} + post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type} # Attach the child to the parent link_child_to_parent(operation) if (post_result.code == 204) return(post_result.code == 204) @@ -634,20 +630,49 @@ return content end # Complex Types def complex_type_to_class(complex_type_xml) - klass_name = qualify_class_name(Helpers.get_namespaced_attribute(complex_type_xml, 'type', 'm').split('.')[-1]) - klass = @classes[klass_name].new + type = Helpers.get_namespaced_attribute(complex_type_xml, 'type', 'm') - # Fill in the properties + is_collection = false + # Extract the class name in case this is a Collection + if type =~ /\(([^)]*)\)/m + type = $~[1] + is_collection = true + collection = [] + end + + klass_name = qualify_class_name(type.split('.')[-1]) + + if is_collection + # extract the elements from the collection + elements = complex_type_xml.xpath(".//d:element", @namespaces) + elements.each do |e| + if type.match(/^Edm/) + collection << parse_value(e.content, type) + else + element = @classes[klass_name].new + fill_complex_type_properties(e, element) + collection << element + end + end + return collection + else + klass = @classes[klass_name].new + # Fill in the properties + fill_complex_type_properties(complex_type_xml, klass) + return klass + end + end + + # Helper method for complex_type_to_class + def fill_complex_type_properties(complex_type_xml, klass) properties = complex_type_xml.xpath(".//*") properties.each do |prop| - klass.send "#{prop.name}=", parse_value(prop) + klass.send "#{prop.name}=", parse_value_xml(prop) end - - return klass end # Field Converters # Handles parsing datetimes from a string @@ -666,34 +691,39 @@ return result end # Parses a value into the proper type based on an xml property element - def parse_value(property_xml) + def parse_value_xml(property_xml) property_type = Helpers.get_namespaced_attribute(property_xml, 'type', 'm') property_null = Helpers.get_namespaced_attribute(property_xml, 'null', 'm') - # Handle a nil property type, this is a string - return property_xml.content if property_type.nil? + if property_type.nil? || (property_type && property_type.match(/^Edm/)) + return parse_value(property_xml.content, property_type, property_null) + end + complex_type_to_class(property_xml) + end + + def parse_value(content, property_type = nil, property_null = nil) # Handle anything marked as null return nil if !property_null.nil? && property_null == "true" - # Handle complex types - return complex_type_to_class(property_xml) if !property_type.match(/^Edm/) + # Handle a nil property type, this is a string + return content if property_type.nil? # Handle integers - return property_xml.content.to_i if property_type.match(/^Edm.Int/) + return content.to_i if property_type.match(/^Edm.Int/) # Handle decimals - return property_xml.content.to_d if property_type.match(/Edm.Decimal/) + return content.to_d if property_type.match(/Edm.Decimal/) # Handle DateTimes # return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/) - return parse_date(property_xml.content) if property_type.match(/Edm.DateTime/) + return parse_date(content) if property_type.match(/Edm.DateTime/) # If we can't parse the value, just return the element's content - property_xml.content + content end # Parses a value into the proper type based on a specified return type def parse_primative_type(value, return_type) return value.to_i if return_type == Fixnum