lib/ruby_odata/service.rb in ruby_odata-0.0.10 vs lib/ruby_odata/service.rb in ruby_odata-0.1.0

- old
+ new

@@ -1,124 +1,201 @@ module OData class Service - attr_reader :classes, :class_metadata, :options + attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports # Creates a new instance of the Service class # # ==== Required Attributes # - service_uri: The root URI of the OData service # ==== Options in options hash # - username: username for http basic auth # - password: password for http basic auth # - verify_ssl: false if no verification, otherwise mode (OpenSSL::SSL::VERIFY_PEER is default) # - additional_params: a hash of query string params that will be passed on all calls + # - eager_partial: true (default) if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed def initialize(service_uri, options = {}) @uri = service_uri.gsub!(/\/?$/, '') - @options = options - @rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] } - @collections = [] - @save_operations = [] - @additional_params = options[:additional_params] || {} + set_options! options + default_instance_vars! + set_namespaces build_collections_and_classes end - + # Handles the dynamic AddTo<EntityName> methods as well as the collections on the service def method_missing(name, *args) # Queries if @collections.include?(name.to_s) root = "/#{name.to_s}" root << "(#{args.join(',')})" unless args.empty? @query = QueryBuilder.new(root, @additional_params) return @query - # Adds + # Adds elsif name.to_s =~ /^AddTo(.*)/ type = $1 if @collections.include?(type) @save_operations << Operation.new("Add", $1, args[0]) else super end + elsif @function_imports.include?(name.to_s) + execute_import_function(name.to_s, args) else super end - end # Queues an object for deletion. To actually remove it from the server, you must call save_changes as well. # # ==== Required Attributes # - obj: The object to mark for deletion # # Note: This method will throw an exception if the +obj+ isn't a tracked entity def delete_object(obj) type = obj.class.to_s - if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil? + if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil? @save_operations << Operation.new("Delete", type, obj) else raise "You cannot delete a non-tracked entity" end end - + # Queues an object for update. To actually update it on the server, you must call save_changes as well. - # + # # ==== Required Attributes # - obj: The object to queue for update # - # Note: This method will throw an exception if the +obj+ isn't a tracked entity + # Note: This method will throw an exception if the +obj+ isn't a tracked entity def update_object(obj) type = obj.class.to_s - if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil? + if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil? @save_operations << Operation.new("Update", type, obj) else raise "You cannot update a non-tracked entity" - end + end end - + # Performs save operations (Create/Update/Delete) against the server def save_changes return nil if @save_operations.empty? result = nil - + if @save_operations.length == 1 - result = single_save(@save_operations[0]) - else - result = batch_save(@save_operations) + result = single_save(@save_operations[0]) + else + result = batch_save(@save_operations) end - - # TODO: We should probably perform a check here + + # TODO: We should probably perform a check here # to make sure everything worked before clearing it out - @save_operations.clear - + @save_operations.clear + return result end - # Performs query operations (Read) against the server - def execute + # Performs query operations (Read) against the server, returns an array of record instances. + def execute result = RestClient::Resource.new(build_query_uri, @rest_options).get - build_classes_from_result(result) + handle_collection_result(result) end - - # Overridden to identify methods handled by method_missing + + # Overridden to identify methods handled by method_missing def respond_to?(method) if @collections.include?(method.to_s) return true - # Adds + # Adds elsif method.to_s =~ /^AddTo(.*)/ type = $1 if @collections.include?(type) return true else super end + # Function Imports + elsif @function_imports.include?(method.to_s) + return true else super end end - + + # Retrieves the next resultset of a partial result (if any). Does not honor the :eager_partial option. + def next + return if not partial? + handle_partial + end + + # Does the most recent collection returned represent a partial collection? Will aways be false if a query hasn't executed, even if the query would have a partial + def partial? + @has_partial + end + + # Lazy loads a navigation property on a model + # + # ==== Required Attributes + # - obj: The object to fill + # - nav_prop: The navigation property to fill + # + # Note: This method will throw an exception if the +obj+ isn't a tracked entity + # Note: This method will throw an exception if the +nav_prop+ isn't a valid navigation property + def load_property(obj, nav_prop) + raise ArgumentError, "You cannot load a property on an entity that isn't tracked" if obj.send(:__metadata).nil? + raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless obj.respond_to?(nav_prop.to_sym) + raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless @class_metadata[obj.class.to_s][nav_prop].nav_prop + results = RestClient::Resource.new(build_load_property_uri(obj, nav_prop), @rest_options).get + prop_results = build_classes_from_result(results) + obj.send "#{nav_prop}=", (singular?(nav_prop) ? prop_results.first : prop_results) + end + + # Adds a child object to a parent object's collection + # + # ==== Required Attributes + # - parent: The parent object + # - nav_prop: The name of the navigation property to add the child to + # - child: The child object + def add_link(parent, nav_prop, child) + raise ArgumentError, "You cannot add a link on an entity that isn't tracked (#{parent.class})" if parent.send(:__metadata).nil? + raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless parent.respond_to?(nav_prop.to_sym) + raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless @class_metadata[parent.class.to_s][nav_prop].nav_prop + raise ArgumentError, "You cannot add a link on a child entity that isn't tracked (#{child.class})" if child.send(:__metadata).nil? + @save_operations << Operation.new("AddLink", nav_prop, parent, child) + end + private + def set_options!(options) + @options = options + if @options[:eager_partial].nil? + @options[:eager_partial] = true + end + @rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] } + @additional_params = options[:additional_params] || {} + @namespace = options[:namespace] + end + + def default_instance_vars! + @collections = {} + @function_imports = {} + @save_operations = [] + @has_partial = false + @next_uri = nil + end + + def set_namespaces + @edmx = Nokogiri::XML(RestClient::Resource.new(build_metadata_uri, @rest_options).get) + @ds_namespaces = { + "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", + "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx", + "ds" => "http://schemas.microsoft.com/ado/2007/08/dataservices", + "atom" => "http://www.w3.org/2005/Atom" + } + + # Get the edm namespace from the edmx + edm_ns = @edmx.xpath("edmx:Edmx/edmx:DataServices/*", @namespaces).first.namespaces['xmlns'].to_s + @ds_namespaces.merge! "edm" => edm_ns + end + # Gets ssl certificate verification mode, or defaults to verify_peer def get_verify_mode if @options[:verify_ssl].nil? return OpenSSL::SSL::VERIFY_PEER else @@ -128,110 +205,194 @@ # Build the classes required by the metadata def build_collections_and_classes @classes = Hash.new @class_metadata = Hash.new # This is used to store property information about a class - - doc = Nokogiri::XML(RestClient::Resource.new(build_metadata_uri, @rest_options).get) - - # Get the edm namespace - edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s - # Fill in the collections instance variable - collections = doc.xpath("//edm:EntityContainer/edm:EntitySet", "edm" => edm_ns) - @collections = collections.collect { |c| c["Name"] } - # Build complex types first, these will be used for entities - complex_types = doc.xpath("//edm:ComplexType", "edm" => edm_ns) || [] + complex_types = @edmx.xpath("//edm:ComplexType", @ds_namespaces) || [] complex_types.each do |c| - name = c['Name'] - props = c.xpath(".//edm:Property", "edm" => edm_ns) + name = qualify_class_name(c['Name']) + props = c.xpath(".//edm:Property", @ds_namespaces) methods = props.collect { |p| p['Name'] } # Standard Properties - @classes[name] = ClassBuilder.new(name, methods, []).build unless @classes.keys.include?(name) + @classes[name] = ClassBuilder.new(name, methods, [], self, @namespace).build unless @classes.keys.include?(name) end - entity_types = doc.xpath("//edm:EntityType", "edm" => edm_ns) + entity_types = @edmx.xpath("//edm:EntityType", @ds_namespaces) entity_types.each do |e| - name = e['Name'] - props = e.xpath(".//edm:Property", "edm" => edm_ns) - @class_metadata[name] = build_property_metadata(props) - methods = props.collect { |p| p['Name'] } # Standard Properties - nprops = e.xpath(".//edm:NavigationProperty", "edm" => edm_ns) - nav_props = nprops.collect { |p| p['Name'] } # Navigation Properties - @classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name) + next if e['Abstract'] == "true" + klass_name = qualify_class_name(e['Name']) + methods = collect_properties(klass_name, e, @edmx) + nav_props = collect_navigation_properties(klass_name, e, @edmx) + @classes[klass_name] = ClassBuilder.new(klass_name, methods, nav_props, self, @namespace).build unless @classes.keys.include?(klass_name) end + + # Fill in the collections instance variable + collections = @edmx.xpath("//edm:EntityContainer/edm:EntitySet", @ds_namespaces) + collections.each do |c| + entity_type = c["EntityType"] + @collections[c["Name"]] = { :edmx_type => entity_type, :type => convert_to_local_type(entity_type) } + end + + build_function_imports end - + + # Parses the function imports and fills the @function_imports collection + def build_function_imports + # Fill in the function imports + functions = @edmx.xpath("//edm:EntityContainer/edm:FunctionImport", @ds_namespaces) + functions.each do |f| + http_method = f.xpath("@m:HttpMethod", @ds_namespaces).first.content + return_type = f["ReturnType"] + inner_return_type = nil + unless return_type.nil? + return_type = (return_type =~ /^Collection/) ? Array : convert_to_local_type(return_type) + if f["ReturnType"] =~ /\((.*)\)/ + inner_return_type = convert_to_local_type($~[1]) + end + end + params = f.xpath("edm:Parameter", @ds_namespaces) + parameters = nil + if params.length > 0 + parameters = {} + params.each do |p| + parameters[p["Name"]] = p["Type"] + end + end + @function_imports[f["Name"]] = { + :http_method => http_method, + :return_type => return_type, + :inner_return_type => inner_return_type, + :parameters => parameters } + end + end + + # Converts the EDMX model type to the local model type + def convert_to_local_type(edmx_type) + return edm_to_ruby_type(edmx_type) if edmx_type =~ /^Edm/ + klass_name = qualify_class_name(edmx_type.split('.').last) + klass_name.camelize.constantize + end + + # Converts a class name to its fully qualified name (if applicable) and returns the new name + def qualify_class_name(klass_name) + unless @namespace.nil? || @namespace.blank? || klass_name.include?('::') + namespaces = @namespace.split(/\.|::/) + namespaces << klass_name + klass_name = namespaces.join '::' + end + klass_name.camelize + end + + # Builds the metadata need for each property for things like feed customizations and navigation properties def build_property_metadata(props) metadata = {} props.each do |property_element| prop_meta = PropertyMetadata.new(property_element) + # If this is a navigation property, we need to add the association to the property metadata + prop_meta.association = Association.new(property_element, @edmx) if prop_meta.nav_prop metadata[prop_meta.name] = prop_meta end metadata end + + # Handle parsing of OData Atom result and return an array of Entry classes + def handle_collection_result(result) + results = build_classes_from_result(result) + while partial? && @options[:eager_partial] + results.concat handle_partial + end + results + end + + # Loops through the standard properties (non-navigation) for a given class and returns the appropriate list of methods + def collect_properties(klass_name, element, doc) + props = element.xpath(".//edm:Property", @ds_namespaces) + @class_metadata[klass_name] = build_property_metadata(props) + methods = props.collect { |p| p['Name'] } + unless element["BaseType"].nil? + base = element["BaseType"].split(".").last() + baseType = doc.xpath("//edm:EntityType[@Name=\"#{base}\"]", @ds_namespaces).first() + props = baseType.xpath(".//edm:Property", @ds_namespaces) + @class_metadata[klass_name].merge!(build_property_metadata(props)) + methods = methods.concat(props.collect { |p| p['Name']}) + end + methods + end + + # Similar to +collect_properties+, but handles the navigation properties + def collect_navigation_properties(klass_name, element, doc) + nav_props = element.xpath(".//edm:NavigationProperty", @ds_namespaces) + @class_metadata[klass_name].merge!(build_property_metadata(nav_props)) + nav_props.collect { |p| p['Name'] } + end # Helper to loop through a result and create an instance for each entity in the results def build_classes_from_result(result) doc = Nokogiri::XML(result) - entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom") - return entry_to_class(entries[0]) if entries.length == 1 + is_links = doc.at_xpath("/ds:links", @ds_namespaces) + return parse_link_results(doc) if is_links + + entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", @ds_namespaces) + + extract_partial(doc) + results = [] entries.each do |entry| results << entry_to_class(entry) end return results end # Converts an XML Entry into a class def entry_to_class(entry) # Retrieve the class name from the fully qualified name (the last string after the last dot) - klass_name = entry.xpath("./atom:category/@term", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1] + klass_name = entry.xpath("./atom:category/@term", @ds_namespaces).to_s.split('.')[-1] # Is the category missing? See if there is a title that we can use to build the class if klass_name.nil? - title = entry.xpath("./atom:title", "atom" => "http://www.w3.org/2005/Atom").first + title = entry.xpath("./atom:title", @ds_namespaces).first return nil if title.nil? 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", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" }).any? + 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, { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" }) + properties = entry.xpath(properties_xpath, @ds_namespaces) - klass = @classes[klass_name].new + klass = @classes[qualify_class_name(klass_name)].new # Fill metadata - meta_id = entry.xpath("./atom:id", "atom" => "http://www.w3.org/2005/Atom")[0].content + 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) end # Fill properties represented outside of the properties collection - @class_metadata[klass_name].select { |k,v| v.fc_keep_in_content == false }.each do |k, meta| + @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" - title = entry.xpath("./atom:title", "atom" => "http://www.w3.org/2005/Atom").first + title = entry.xpath("./atom:title", @ds_namespaces).first klass.send "#{meta.name}=", title.content elsif meta.fc_target_path == "SyndicationSummary" - summary = entry.xpath("./atom:summary", "atom" => "http://www.w3.org/2005/Atom").first + summary = entry.xpath("./atom:summary", @ds_namespaces).first klass.send "#{meta.name}=", summary.content end end - inline_links = entry.xpath("./atom:link[m:inline]", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "atom" => "http://www.w3.org/2005/Atom" }) + inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces) - for link in inline_links - inline_entries = link.xpath(".//atom:entry", "atom" => "http://www.w3.org/2005/Atom") + 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) @@ -245,17 +406,43 @@ # Add the property to the temp collection inline_classes << inline_klass end # Assign the array of classes to the property - property_name = link.xpath("@title", "atom" => "http://www.w3.org/2005/Atom") + property_name = link.xpath("@title", @ds_namespaces) klass.send "#{property_name}=", inline_classes end end klass end + + # 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? + @next_uri = next_links[0]['href'] if @has_partial + end + + def handle_partial + if @next_uri + result = RestClient::Resource.new(@next_uri, @rest_options).get + results = handle_collection_result(result) + end + results + end + + # Handle link results + def parse_link_results(doc) + uris = doc.xpath("/ds:links/ds:uri", @ds_namespaces) + results = [] + uris.each do |uri_el| + link = uri_el.content + results << URI.parse(link) + end + results + end # Build URIs def build_metadata_uri uri = "#{@uri}/$metadata" uri << "?#{@additional_params.to_query}" unless @additional_params.empty? @@ -267,28 +454,66 @@ def build_save_uri(operation) uri = "#{@uri}/#{operation.klass_name}" uri << "?#{@additional_params.to_query}" unless @additional_params.empty? uri end + def build_add_link_uri(operation) + uri = "#{operation.klass.send(:__metadata)[:uri]}" + uri << "/$links/#{operation.klass_name}" + uri << "?#{@additional_params.to_query}" unless @additional_params.empty? + uri + end def build_resource_uri(operation) uri = operation.klass.send(:__metadata)[:uri] uri << "?#{@additional_params.to_query}" unless @additional_params.empty? uri end def build_batch_uri uri = "#{@uri}/$batch" uri << "?#{@additional_params.to_query}" unless @additional_params.empty? uri end + def build_load_property_uri(obj, property) + uri = obj.__metadata[:uri] + uri << "/#{property}" + uri + end + def build_function_import_uri(name, params) + uri = "#{@uri}/#{name}" + params.merge! @additional_params + uri << "?#{params.to_query}" unless params.empty? + uri + end def build_inline_class(klass, entry, property_name) # Build the class inline_klass = entry_to_class(entry) # Add the property klass.send "#{property_name}=", inline_klass end + + # Used to link a child object to its parent and vice-versa after a add_link operation + def link_child_to_parent(operation) + child_collection = operation.klass.send("#{operation.klass_name}") || [] + child_collection << operation.child_klass + operation.klass.send("#{operation.klass_name}=", child_collection) + + # Attach the parent to the child + parent_meta = @class_metadata[operation.klass.class.to_s][operation.klass_name] + child_meta = @class_metadata[operation.child_klass.class.to_s] + # Find the matching relationship on the child object + child_properties = Helpers.normalize_to_hash( + child_meta.select { |k, prop| + prop.nav_prop && + prop.association.relationship == parent_meta.association.relationship }) + + child_property_to_set = child_properties.keys.first # There should be only one match + # TODO: Handle many to many scenarios where the child property is an enumerable + operation.child_klass.send("#{child_property_to_set}=", operation.klass) + end + 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} @@ -299,43 +524,51 @@ update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => :json} 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) - end + 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} + + # Attach the child to the parent + link_child_to_parent(operation) if (post_result.code == 204) + + return(post_result.code == 204) + end end # Batch Saves def generate_guid rand(36**12).to_s(36).insert(4, "-").insert(9, "-") end - def batch_save(operations) + def batch_save(operations) batch_num = generate_guid changeset_num = generate_guid batch_uri = build_batch_uri body = build_batch_body(operations, batch_num, changeset_num) - result = RestClient::Resource.new( batch_uri, @rest_options).post body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"} # TODO: More result validation needs to be done. # The result returns HTTP 202 even if there is an error in the batch return (result.code == 202) end def build_batch_body(operations, batch_num, changeset_num) - # Header + # Header body = "--batch_#{batch_num}\n" body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n" # Operations operations.each do |operation| body << build_batch_operation(operation, changeset_num) body << "\n" end - # Footer + # Footer body << "\n\n--changeset_#{changeset_num}--\n" body << "--batch_#{batch_num}--" return body end @@ -346,11 +579,11 @@ content = "--changeset_#{changeset_num}\n" content << "Content-Type: application/http\n" content << "Content-Transfer-Encoding: binary\n\n" - if operation.kind == "Add" + if operation.kind == "Add" save_uri = "#{@uri}/#{operation.klass_name}" json_klass = operation.klass.to_json(:type => :add) content << "POST #{save_uri} HTTP/1.1\n" content << accept_headers @@ -365,18 +598,26 @@ elsif operation.kind == "Delete" delete_uri = operation.klass.send(:__metadata)[:uri] content << "DELETE #{delete_uri} HTTP/1.1\n" content << accept_headers - end + elsif + save_uri = build_add_link_uri(operation) + json_klass = operation.child_klass.to_json(:type => :link) + content << "POST #{save_uri} HTTP/1.1\n" + content << accept_headers + content << json_klass + link_child_to_parent(operation) + end + return content end # Complex Types def complex_type_to_class(complex_type_xml) - klass_name = complex_type_xml.attr('type').split('.')[-1] + klass_name = qualify_class_name(complex_type_xml.attr('type').split('.')[-1]) klass = @classes[klass_name].new # Fill in the properties properties = complex_type_xml.xpath(".//*") properties.each do |prop| @@ -385,10 +626,29 @@ return klass end # Field Converters + + # Handles parsing datetimes from a string + def parse_date(sdate) + # Assume this is UTC if no timezone is specified + sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/) + + # This is to handle older versions of Ruby (e.g. ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]) + # See http://makandra.com/notes/1017-maximum-representable-value-for-a-ruby-time-object + # In recent versions of Ruby, Time has a much larger range + begin + result = Time.parse(sdate) + rescue ArgumentError + result = DateTime.parse(sdate) + end + + return result + end + + # Parses a value into the proper type based on an xml property element def parse_value(property_xml) property_type = property_xml.attr('type') property_null = property_xml.attr('null') # Handle a nil property type, this is a string @@ -406,29 +666,82 @@ # Handle decimals return property_xml.content.to_d if property_type.match(/Edm.Decimal/) # Handle DateTimes # return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/) - if property_type.match(/Edm.DateTime/) - sdate = property_xml.content + return parse_date(property_xml.content) if property_type.match(/Edm.DateTime/) - # Assume this is UTC if no timezone is specified - sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/) - - # This is to handle older versions of Ruby (e.g. ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]) - # See http://makandra.com/notes/1017-maximum-representable-value-for-a-ruby-time-object - # In recent versions of Ruby, Time has a much larger range - begin - result = Time.parse(sdate) - rescue ArgumentError - result = DateTime.parse(sdate) - end - - return result - end - # If we can't parse the value, just return the element's content property_xml.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 + return value.to_d if return_type == Float + return parse_date(value.to_s) if return_type == Time + return value.to_s + end + + # Converts an edm type (string) to a ruby type + def edm_to_ruby_type(edm_type) + return String if edm_type =~ /Edm.String/ + return Fixnum if edm_type =~ /^Edm.Int/ + return Float if edm_type =~ /Edm.Decimal/ + return Time if edm_type =~ /Edm.DateTime/ + return String + end + + # Method Missing Handlers + + # Executes an import function + def execute_import_function(name, *args) + func = @function_imports[name] + + # Check the args making sure that more weren't passed in than the function needs + param_count = func[:parameters].nil? ? 0 : func[:parameters].count + arg_count = args.nil? ? 0 : args[0].count + if arg_count > param_count + raise ArgumentError, "wrong number of arguments (#{arg_count} for #{param_count})" + end + + # Convert the parameters to a hash + params = {} + func[:parameters].keys.each_with_index { |key, i| params[key] = args[0][i] } unless func[:parameters].nil? + + function_uri = build_function_import_uri(name, params) + result = RestClient::Resource.new(function_uri, @rest_options).send(func[:http_method].downcase, {}) + + # Is this a 204 (No content) result? + return true if result.code == 204 + + # No? Then we need to parse the results. There are 4 kinds... + if func[:return_type] == Array + # a collection of entites + return build_classes_from_result(result) if @classes.include?(func[:inner_return_type].to_s) + # a collection of native types + elements = Nokogiri::XML(result).xpath("//ds:element", @ds_namespaces) + results = [] + elements.each do |e| + results << parse_primative_type(e.content, func[:inner_return_type]) + end + return results + end + + # a single entity + if @classes.include?(func[:return_type].to_s) + entry = Nokogiri::XML(result).xpath("atom:entry[not(ancestor::atom:entry)]", @ds_namespaces) + return entry_to_class(entry) + end + + # or a single native type + unless func[:return_type].nil? + e = Nokogiri::XML(result).xpath("/*").first + return parse_primative_type(e.content, func[:return_type]) + end + + # Nothing could be parsed, so just return if we got a 200 or not + return (result.code == 200) end # Helpers def singular?(value) value.singularize == value \ No newline at end of file