Skip to content

Commit

Permalink
Added support for querying links
Browse files Browse the repository at this point in the history
  • Loading branch information
visoft committed Nov 11, 2011
1 parent 228f99a commit 7de7dce
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 64 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@

=== 0.1.0
* New Features
* Added support for single layer inheritance (thanks to http://odetocode.com/Blogs/scott/archive/2010/07/11/odata-and-ruby.aspx)
* Added support for single layer inheritance (thanks to http://odetocode.com/Blogs/scott/archive/2010/07/11/odata-and-ruby.aspx)
* Added support for querying links (see https://github.com/visoft/ruby_odata/issues/10)
5 changes: 5 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ Top allows you only retrieve the top X number of records when querying. This is
svc.Products.top(5)
products = svc.execute # => returns only the first 5 items

=== Navigation Property Links Only Query
OData allows you to {query navigation properties and only return the links for the entities}[http://www.odata.org/developers/protocols/uri-conventions#AddressingLinksBetweenEntries] (instead of the data)
svc.Categories(1).links("Products")
product_links = svc.execute # => returns URIs for the products under the Category with an ID of 1

=== Partial feeds
OData allows services to do server-side paging in Atom by defining a next link. The default behavior is to repeatedly consume partial feeds until the result set is complete.

Expand Down
20 changes: 15 additions & 5 deletions features/query_builder.feature
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,18 @@ Scenario: Top should be able to be used along with skip for paging
| Product 4 |







# Links
@current
Scenario: Navigation Properties should be able to represented as links
Given I call "AddToCategories" on the service with a new "Category" object with Name: "Test Category"
And I save changes
And the following Products exist:
| Name | Category |
| Product 1 | @@LastSave.first |
| Product 2 | @@LastSave.first |
| Product 3 | @@LastSave.first |
When I call "Categories" on the service with args: "1"
And I ask for the links for "Products"
And I run the query
Then the result count should be 3
Then the method "path" on the result object should equal: "/SampleService/Entities.svc/Products(1)"
77 changes: 58 additions & 19 deletions features/step_definitions/service_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ def first(results)
BASICAUTH_URL = "http://#{WEBSERVER}:#{HTTP_PORT_NUMBER}/SampleService/BasicAuth/Entities.svc"
HTTPS_BASICAUTH_URL = "https://#{WEBSERVER}:#{HTTPS_PORT_NUMBER}/SampleService/BasicAuth/Entities.svc"

# Helper methods
def handle_last_save_fields(val)
if val =~ /^@@LastSave.first$/
val = @saved_result.first
end
if val =~ /^@@LastSave$/
val = @saved_result
end
val
end

def parse_fields_string(fields)
fields_hash = {}

if !fields.nil?
fields.split(', ').each do |field|
if field =~ /^(?:(\w+): "(.*)")$/
key = $1
val = handle_last_save_fields($2)

fields_hash.merge!({key => val})
end
end
end
fields_hash
end

def parse_fields_hash(fields)
fields_hash = {}

if !fields.nil?
fields.each do |key, val|
val = handle_last_save_fields(val)
fields_hash.merge!({key => val})
end
end
fields_hash
end

When /^(.*) first (.*)$/ do |step, results|
first(results) { When step }
end
Expand Down Expand Up @@ -89,6 +128,10 @@ def first(results)
@service_result.send(method.to_sym).to_s.should == value
end

Then /^the method "([^\"]*)" on the result object should equal: "([^\"]*)"$/ do |method, value|
@service_result.first.send(method.to_sym).to_s.should == value
end

Then /^the method "([^\"]*)" on the result should be nil$/ do |method|
@service_result.send(method.to_sym).should == nil
end
Expand Down Expand Up @@ -121,30 +164,17 @@ def first(results)
@service_query.top(top)
end

When /^I ask for the links for "([^\"]*)"$/ do |nav_prop|
@service_query.links(nav_prop)
end

Then /^the method "([^\"]*)" on the result should be of type "([^\"]*)"$/ do |method, type|
result = @service_result.send(method.to_sym)
result.class.to_s.should == type
end

Given /^I call "([^\"]*)" on the service with a new "([^\"]*)" object(?: with (.*))?$/ do |method, object, fields|
fields_hash = {}

if !fields.nil?
fields.split(', ').each do |field|
if field =~ /^(?:(\w+): "(.*)")$/
key = $1
val = $2
if val =~ /^@@LastSave.first$/
val = @saved_result.first
end
if val =~ /^@@LastSave$/
val = @saved_result
end

fields_hash.merge!({ key => val })
end
end
end
fields_hash = parse_fields_string(fields)

obj = object.constantize.send(:make, fields_hash)
@service.send(method.to_sym, obj)
Expand Down Expand Up @@ -197,11 +227,12 @@ def first(results)
results.should == []
end


Given /^the following (.*) exist:$/ do |plural_factory, table|
# table is a Cucumber::Ast::Table
factory = plural_factory.singularize
table.hashes.map do |hash|
obj = factory.constantize.send(:make, hash)
obj = factory.constantize.send(:make, parse_fields_hash(hash))
@service.send("AddTo#{plural_factory}", obj)
@service.save_changes
end
Expand Down Expand Up @@ -326,4 +357,12 @@ def first(results)
else
@service_result.send(methods[0]).send(methods[1]).xmlschema(3).should == @stored_query_result.send(methods[0]).send(methods[1]).xmlschema(3)
end
end

Then /^show me the results$/ do
puts @service_result
end

Then /^the result count should be (\d+)$/ do |expected_count|
@service_result.count.should eq expected_count.to_i
end
19 changes: 19 additions & 0 deletions lib/ruby_odata/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def initialize(root, additional_params = {})
@order_bys = []
@skip = nil
@top = nil
@links = nil
@additional_params = additional_params
end

Expand Down Expand Up @@ -89,10 +90,28 @@ def top(num)
self
end

# Used to return links instead of actual objects
# ==== Required Attributes
# - navigation_property: The NavigationProperty name to retrieve the links for
#
# ==== Example
# svc.Categories(1).links("Products")
# product_links = svc.execute # => returns URIs for the products under the Category with an ID of 1
def links(navigation_property)
@navigation_property = navigation_property
self
end

# Builds the query URI (path, not including root) incorporating expands, filters, etc.
# This is used internally when the execute method is called on the service
def query
q = @root.clone

# Handle links queries, this isn't just a standard query option
if @navigation_property
q << "/$links/#{@navigation_property}"
end

query_options = []
query_options << "$expand=#{@expands.join(',')}" unless @expands.empty?
query_options << "$filter=#{@filters.join('+and+')}" unless @filters.empty?
Expand Down
16 changes: 15 additions & 1 deletion lib/ruby_odata/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ def collect_properties(klass_name, edm_ns, element, doc)
# 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)

is_links = doc.at_xpath("/ds:links", "ds" => "http://schemas.microsoft.com/ado/2007/08/dataservices")
return parse_link_results(doc) if is_links

entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom")

extract_partial(doc)
Expand Down Expand Up @@ -320,7 +324,17 @@ def handle_partial
end
results
end


# Handle link results
def parse_link_results(doc)
uris = doc.xpath("/ds:links/ds:uri", "ds" => "http://schemas.microsoft.com/ado/2007/08/dataservices")
results = []
uris.each do |uri_el|
link = uri_el.content
results << URI.parse(link)
end
results
end

# Build URIs
def build_metadata_uri
Expand Down
45 changes: 45 additions & 0 deletions spec/fixtures/edmx_categories_products.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
<Schema Namespace="Model" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityContainer Name="ModelContainer" p7:LazyLoadingEnabled="true" m:IsDefaultEntityContainer="true" xmlns:p7="http://schemas.microsoft.com/ado/2009/02/edm/annotation">
<FunctionImport Name="CleanDatabaseForTesting" m:HttpMethod="POST" />
<EntitySet Name="Products" EntityType="Model.Product" />
<EntitySet Name="Categories" EntityType="Model.Category" />
<AssociationSet Name="CategoryProduct" Association="Model.CategoryProduct">
<End Role="Category" EntitySet="Categories" />
<End Role="Product" EntitySet="Products" />
</AssociationSet>
</EntityContainer>
<EntityType Name="Product">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity" xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="Description" Type="Edm.String" Nullable="false" />
<Property Name="Price" Type="Edm.Decimal" Nullable="false" />
<Property Name="AuditFields" Type="Model.AuditFields" Nullable="false" />
<Property Name="DiscontinuedDate" Type="Edm.DateTime" Nullable="true" />
<NavigationProperty Name="Category" Relationship="Model.CategoryProduct" FromRole="Product" ToRole="Category" />
</EntityType>
<ComplexType Name="AuditFields">
<Property Name="CreateDate" Type="Edm.DateTime" Nullable="false" />
<Property Name="ModifiedDate" Type="Edm.DateTime" Nullable="false" />
<Property Name="CreatedBy" Type="Edm.String" Nullable="true" />
</ComplexType>
<EntityType Name="Category">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity" xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<NavigationProperty Name="Products" Relationship="Model.CategoryProduct" FromRole="Category" ToRole="Product" />
</EntityType>
<Association Name="CategoryProduct">
<End Role="Category" Type="Model.Category" Multiplicity="1" />
<End Role="Product" Type="Model.Product" Multiplicity="*" />
</Association>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
6 changes: 6 additions & 0 deletions spec/fixtures/links/result_links_query.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?>
<links xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices">
<uri>http://test.com/SampleService/Entities.svc/Products(1)</uri>
<uri>http://test.com/SampleService/Entities.svc/Products(2)</uri>
<uri>http://test.com/SampleService/Entities.svc/Products(3)</uri>
</links>
11 changes: 11 additions & 0 deletions spec/query_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ module OData
builder.top(10)
builder.query.should eq "Products?$top=10&x=1&y=2"
end
it "should properly handle queries for links" do
builder = QueryBuilder.new 'Categories(1)'
builder.links('Products')
builder.query.should eq "Categories(1)/$links/Products"
end
it "should properly handle queries for links with additional operations" do
builder = QueryBuilder.new 'Categories(1)'
builder.links('Products')
builder.top(5)
builder.query.should eq "Categories(1)/$links/Products?$top=5"
end
end
end
end
Loading

0 comments on commit 7de7dce

Please sign in to comment.