ruby_odata 0.1.3 → 0.1.4

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.
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore CHANGED
@@ -10,9 +10,12 @@ doc/*
10
10
  *.gem
11
11
  .bundle
12
12
  Gemfile.lock
13
+ Gemfile.*.lock
13
14
  pkg/*
14
15
  test/applicationhost.config
15
16
  .rvmrc
17
+ .ruby-version
18
+ .ruby-gemset
16
19
  .DS_Store
17
20
  *ReSharper*
18
21
  *.suo
@@ -20,3 +23,4 @@ obj
20
23
  *.pdb
21
24
  test/RubyODataService/RubyODataService/bin/*.xml
22
25
  test/RubyODataService/RubyODataService/App_Data/*.sdf
26
+ coverage/*
@@ -0,0 +1,10 @@
1
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('1.9') && ENV['COVERAGE']
2
+ require 'simplecov'
3
+ require 'coveralls'
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
6
+ SimpleCov::Formatter::HTMLFormatter
7
+ ]
8
+
9
+ SimpleCov.start
10
+ end
@@ -1,5 +1,10 @@
1
- rvm:
2
- - 1.8.7
3
- - 1.9.2
4
- - 1.9.3
5
- script: "bundle exec rake"
1
+ matrix:
2
+ include:
3
+ - rvm: 1.8.7
4
+ gemfile: gemfiles/Gemfile.ruby187
5
+ - rvm: 1.9.3
6
+ gemfile: Gemfile
7
+ - rvm: 2.0.0
8
+ gemfile: Gemfile
9
+
10
+ script: "bundle exec rake test_with_coveralls"
@@ -115,4 +115,24 @@
115
115
  * Persists the additional_params for partial calls (thanks [@levelboy](https://github.com/levelboy))
116
116
 
117
117
  * Other
118
- * Specified v2.3.4 of the addressable gem since there was a bug when testing ruby_odata against Ruby 1.8.7
118
+ * Specified v2.3.4 of the addressable gem since there was a bug when testing ruby_odata against Ruby 1.8.7
119
+
120
+ ### 0.1.4
121
+ * New Features
122
+ * Added option to override content type used for json updates ([issue 29](https://github.com/visoft/ruby_odata/pull/29), thanks [@sigmunau](https://github.com/sigmunau))
123
+
124
+ * Bug Fixes
125
+ * Fixed issue with building a collection of complex types ([issue 26](https://github.com/visoft/ruby_odata/issues/26))
126
+ * A collection of complex types is now returned as an array ([issue 26](https://github.com/visoft/ruby_odata/issues/26))
127
+ * Fixed issue with building a child collection of native types ([issue 27](https://github.com/visoft/ruby_odata/issues/27))
128
+ * Corrected problem with addressable not being referenced
129
+ * Fixed issue with building nested expands ([issue 24](https://github.com/visoft/ruby_odata/pull/24), thanks [@joshuap](https://github.com/joshuap))
130
+ * Edm.Int64 is now formatted as a string, according to odata json spec ([issue 29](https://github.com/visoft/ruby_odata/pull/29), thanks [@sigmunau](https://github.com/sigmunau))
131
+ * Fixed formatting of collections for json output ([issue 29](https://github.com/visoft/ruby_odata/pull/29), thanks [@sigmunau](https://github.com/sigmunau))
132
+ * Fixed handling exceptions that are not http exceptions ([issue 29](https://github.com/visoft/ruby_odata/pull/29), thanks [@sigmunau](https://github.com/sigmunau))
133
+ * Fixed parsing of null strings ([issue 29](https://github.com/visoft/ruby_odata/pull/29), thanks [@sigmunau](https://github.com/sigmunau))
134
+
135
+ * Other
136
+ * Updated the [VCR](https://github.com/myronmarston/vcr) and [WebMock](https://github.com/bblimke/webmock) gems to the latest versions (used for testing)
137
+ * Specified activesupport ~> 3.0 (in gemfiles/ruby187) for Ruby 1.8.7 as activesupport 4 doesn't support Ruby < 1.9.3
138
+
data/Rakefile CHANGED
@@ -13,6 +13,23 @@ Cucumber::Rake::Task.new(:features) do |t|
13
13
  t.cucumber_opts = "features --format progress"
14
14
  end
15
15
 
16
-
17
16
  Bundler::GemHelper.install_tasks
18
- task :default => [:spec, :features]
17
+ task :default => [:spec, :features]
18
+
19
+ desc "Run with code coverage"
20
+ task :coverage do
21
+ ENV['COVERAGE'] = 'true' if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('1.9')
22
+
23
+ Rake::Task["spec"].execute
24
+ Rake::Task["features"].execute
25
+ end
26
+
27
+ desc "Run test with coveralls"
28
+ task :test_with_coveralls => [:coverage, 'coveralls_push_workaround']
29
+ task :coveralls_push_workaround do
30
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('1.9')
31
+ require 'coveralls/rake/task'
32
+ Coveralls::RakeTask.new
33
+ Rake::Task["coveralls:push"].invoke
34
+ end
35
+ end
@@ -20,14 +20,6 @@ Given /^a HTTP ODataService exists$/ do
20
20
  end
21
21
  end
22
22
 
23
- Given /^a HTTP BasicAuth ODataService exists$/ do
24
- @service = OData::Service.new(BASICAUTH_URL)
25
- end
26
-
27
- Given /^a HTTPS BasicAuth ODataService exists$/ do
28
- @service = OData::Service.new(HTTPS_BASICAUTH_URL)
29
- end
30
-
31
23
  Given /^a HTTP BasicAuth ODataService exists using username "([^\"]*)" and password "([^\"]*)"$/ do |username, password|
32
24
  @service = OData::Service.new(BASICAUTH_URL, { :username => username, :password => password })
33
25
  end
@@ -36,14 +28,6 @@ Given /^a HTTP BasicAuth ODataService exists using username "([^\"]*)" and passw
36
28
  lambda { @service = OData::Service.new(BASICAUTH_URL, { :username => username, :password => password }) }.should raise_error(msg)
37
29
  end
38
30
 
39
- Given /^a HTTP BasicAuth ODataService exists it should throw an exception with message containing "([^\"]*)"$/ do |msg|
40
- lambda { @service = OData::Service.new(BASICAUTH_URL) }.should raise_error(/#{msg}.*/)
41
- end
42
-
43
- Given /^a HTTPS BasicAuth ODataService exists it should throw an exception with message containing "([^"]*)"$/ do |msg|
44
- lambda { @service = OData::Service.new(HTTPS_BASICAUTH_URL) }.should raise_error(/#{msg}.*/)
45
- end
46
-
47
31
  Given /^a HTTP BasicAuth ODataService exists it should throw an exception with message "([^\"]*)"$/ do |msg|
48
32
  lambda { @service = OData::Service.new(BASICAUTH_URL) }.should raise_error(msg)
49
33
  end
@@ -56,10 +40,6 @@ When /^I call "([^\"]*)" on the service$/ do |method|
56
40
  @service_query = @service.send(method)
57
41
  end
58
42
 
59
- Then /^the result should be "([^\"]*)"$/ do |result|
60
- @service_result.should eq result
61
- end
62
-
63
43
  Then /^the integer result should be ([^\"]*)$/ do |result|
64
44
  @service_result.should eq result.to_i
65
45
  end
@@ -216,27 +196,6 @@ Then /^the result should be:$/ do |table|
216
196
  table.diff!(result_table)
217
197
  end
218
198
 
219
- Then /^the save result should be:$/ do |table|
220
- # table is a Cucumber::Ast::Table
221
-
222
- fields = table.hashes[0].keys
223
-
224
- # Build an array of hashes so that we can compare tables
225
- results = []
226
-
227
- @saved_result.each do |result|
228
- obj_hash = Hash.new
229
- fields.each do |field|
230
- obj_hash[field] = result.send(field)
231
- end
232
- results << obj_hash
233
- end
234
-
235
- result_table = Cucumber::Ast::Table.new(results)
236
-
237
- table.diff!(result_table)
238
- end
239
-
240
199
  Then /^a class named "([^\"]*)" should exist$/ do |klass_name|
241
200
  (Object.const_defined? klass_name).should eq true
242
201
  end
@@ -261,14 +220,6 @@ When /^I set "([^\"]*)" on the result's method "([^\"]*)" to "([^\"]*)"$/ do |pr
261
220
  end
262
221
 
263
222
  # Type tests
264
- Then /^the "([^\"]*)" method should return a (.*)/ do |method_name, type|
265
- methods = method_name.split '.'
266
- if methods.length == 1
267
- @service_result.send(method_name).class.to_s.should eq type
268
- else
269
- @service_result.send(methods[0]).send(methods[1]).class.to_s.should eq type
270
- end
271
- end
272
223
  Then /^the "([^\"]*)" method on the object should return a (.*)/ do |method_name, type|
273
224
  methods = method_name.split '.'
274
225
  if methods.length == 1
@@ -284,16 +235,9 @@ end
284
235
 
285
236
  Then /^the new query result's time "([^\"]*)" should equal the saved query result$/ do |method_name|
286
237
  methods = method_name.split '.'
287
- if methods.length == 1
288
- @service_result.send(method_name).xmlschema(3).should eq @stored_query_result.send(method_name).xmlschema(3)
289
- else
290
- @service_result.send(methods[0]).send(methods[1]).xmlschema(3).should eq @stored_query_result.send(methods[0]).send(methods[1]).xmlschema(3)
291
- end
238
+ @service_result.send(methods[0]).send(methods[1]).xmlschema(3).should eq @stored_query_result.send(methods[0]).send(methods[1]).xmlschema(3)
292
239
  end
293
240
 
294
- Then /^show me the results$/ do
295
- puts @service_result
296
- end
297
241
 
298
242
  Then /^the result count should be (\d+)$/ do |expected_count|
299
243
  @service_result.count.should eq expected_count.to_i
@@ -313,4 +257,4 @@ end
313
257
 
314
258
  When /^(.*) within a cassette named "([^"]*)"$/ do |the_step, cassette_name|
315
259
  VCR.use_cassette(cassette_name) { step the_step }
316
- end
260
+ end
@@ -1,4 +1,9 @@
1
1
  lib = File.expand_path(File.join(File.dirname(__FILE__), "../..", "lib"))
2
2
 
3
3
  require lib + '/ruby_odata'
4
- require "machinist"
4
+ require 'machinist'
5
+
6
+ require 'simplecov'
7
+ # require 'coveralls'
8
+
9
+ # Coveralls.wear_merged!
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec :path => '../'
4
+
5
+ gem 'nokogiri', '~> 1.5.10'
6
+ gem 'activesupport', '~> 3.0'
@@ -12,6 +12,7 @@ require "nokogiri"
12
12
  require "bigdecimal"
13
13
  require "bigdecimal/util"
14
14
  require "backports"
15
+ require "addressable/uri"
15
16
 
16
17
  require lib + "/ruby_odata/exceptions"
17
18
  require lib + "/ruby_odata/association"
@@ -110,6 +110,20 @@ module OData
110
110
  end
111
111
  end
112
112
 
113
+ props = self.class.properties
114
+
115
+ # Convert a Int64 to a string for serialization (to match Edm.Int64)
116
+ bigints = vars.find_all { |o| props[o[0]] && props[o[0]].type == "Edm.Int64" } || []
117
+ bigints.each do |i|
118
+ vars[i[0]] = i[1].to_s
119
+ end
120
+
121
+ # Convert Arrays into proper Collections
122
+ collections = vars.find_all { |o| o[1].class == Array } || []
123
+ collections.each do |c|
124
+ vars[c[0]] = { '__metadata' => { 'type' => props[c[0]].type }, 'results' => c[1] }
125
+ end
126
+
113
127
  # Convert a BigDecimal to a string for serialization (to match Edm.Decimal)
114
128
  decimals = vars.find_all { |o| o[1].class == BigDecimal } || []
115
129
  decimals.each do |d|
@@ -1,7 +1,7 @@
1
1
  module OData
2
2
  # The main service class, also known as a *Context*
3
3
  class Service
4
- attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports
4
+ attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports, :response
5
5
  # Creates a new instance of the Service class
6
6
  #
7
7
  # @param [String] service_uri the root URI of the OData service
@@ -97,9 +97,9 @@ class Service
97
97
  # Performs query operations (Read) against the server.
98
98
  # Typically this returns an array of record instances, except in the case of count queries
99
99
  def execute
100
- result = RestClient::Resource.new(build_query_uri, @rest_options).get
101
- return Integer(result) if result =~ /^\d+$/
102
- handle_collection_result(result)
100
+ @response = RestClient::Resource.new(build_query_uri, @rest_options).get
101
+ return Integer(@response) if @response =~ /^\d+$/
102
+ handle_collection_result(@response)
103
103
  end
104
104
 
105
105
  # Overridden to identify methods handled by method_missing
@@ -176,6 +176,7 @@ class Service
176
176
  @rest_options.merge!(options[:rest_options] || {})
177
177
  @additional_params = options[:additional_params] || {}
178
178
  @namespace = options[:namespace]
179
+ @json_type = options[:json_type] || :json
179
180
  end
180
181
 
181
182
  def default_instance_vars!
@@ -312,7 +313,7 @@ class Service
312
313
 
313
314
  # Handles errors from the OData service
314
315
  def handle_exception(e)
315
- raise e unless e.response
316
+ raise e unless defined? e.response
316
317
 
317
318
  code = e.http_code
318
319
  error = Nokogiri::XML(e.response)
@@ -375,11 +376,7 @@ class Service
375
376
 
376
377
  return nil if klass_name.nil?
377
378
 
378
- # If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL
379
- # have properties that are ancestors of m:inline. Check if there is an m:inline child to determine the xpath query to use
380
- has_inline = entry.xpath(".//m:inline", @ds_namespaces).any?
381
- properties_xpath = has_inline ? ".//m:properties[not(ancestor::m:inline)]/*" : ".//m:properties/*"
382
- properties = entry.xpath(properties_xpath, @ds_namespaces)
379
+ properties = entry.xpath("./atom:content/m:properties/*", @ds_namespaces)
383
380
 
384
381
  klass = @classes[qualify_class_name(klass_name)].new
385
382
 
@@ -390,7 +387,7 @@ class Service
390
387
  # Fill properties
391
388
  for prop in properties
392
389
  prop_name = prop.name
393
- klass.send "#{prop_name}=", parse_value(prop)
390
+ klass.send "#{prop_name}=", parse_value_xml(prop)
394
391
  end
395
392
 
396
393
  # Fill properties represented outside of the properties collection
@@ -407,15 +404,14 @@ class Service
407
404
  inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces)
408
405
 
409
406
  for link in inline_links
410
- inline_entries = link.xpath(".//atom:entry", @ds_namespaces)
411
-
412
407
  # TODO: Use the metadata's associations to determine the multiplicity instead of this "hack"
413
408
  property_name = link.attributes['title'].to_s
414
- if inline_entries.length == 1 && singular?(property_name)
415
- inline_klass = build_inline_class(klass, inline_entries[0], property_name)
409
+ if singular?(property_name)
410
+ inline_entry = link.xpath("./m:inline/atom:entry", @ds_namespaces).first
411
+ inline_klass = build_inline_class(klass, inline_entry, property_name)
416
412
  klass.send "#{property_name}=", inline_klass
417
413
  else
418
- inline_classes = []
414
+ inline_classes, inline_entries = [], link.xpath("./m:inline/atom:feed/atom:entry", @ds_namespaces)
419
415
  for inline_entry in inline_entries
420
416
  # Build the class
421
417
  inline_klass = entry_to_class(inline_entry)
@@ -438,7 +434,7 @@ class Service
438
434
  next_links = doc.xpath('//atom:link[@rel="next"]', @ds_namespaces)
439
435
  @has_partial = next_links.any?
440
436
  if @has_partial
441
- uri = Addressable::URI.parse(next_links[0]['href'])
437
+ uri = Addressable::URI.parse(next_links[0]['href'])
442
438
  uri.query_values = uri.query_values.merge @additional_params unless @additional_params.empty?
443
439
  @next_uri = uri.to_s
444
440
  end
@@ -537,12 +533,12 @@ class Service
537
533
  if operation.kind == "Add"
538
534
  save_uri = build_save_uri(operation)
539
535
  json_klass = operation.klass.to_json(:type => :add)
540
- post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json}
536
+ post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type}
541
537
  return build_classes_from_result(post_result)
542
538
  elsif operation.kind == "Update"
543
539
  update_uri = build_resource_uri(operation)
544
540
  json_klass = operation.klass.to_json
545
- update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => :json}
541
+ update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => @json_type}
546
542
  return (update_result.code == 204)
547
543
  elsif operation.kind == "Delete"
548
544
  delete_uri = build_resource_uri(operation)
@@ -551,7 +547,7 @@ class Service
551
547
  elsif operation.kind == "AddLink"
552
548
  save_uri = build_add_link_uri(operation)
553
549
  json_klass = operation.child_klass.to_json(:type => :link)
554
- post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json}
550
+ post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type}
555
551
 
556
552
  # Attach the child to the parent
557
553
  link_child_to_parent(operation) if (post_result.code == 204)
@@ -636,16 +632,45 @@ class Service
636
632
 
637
633
  # Complex Types
638
634
  def complex_type_to_class(complex_type_xml)
639
- klass_name = qualify_class_name(Helpers.get_namespaced_attribute(complex_type_xml, 'type', 'm').split('.')[-1])
640
- klass = @classes[klass_name].new
635
+ type = Helpers.get_namespaced_attribute(complex_type_xml, 'type', 'm')
636
+
637
+ is_collection = false
638
+ # Extract the class name in case this is a Collection
639
+ if type =~ /\(([^)]*)\)/m
640
+ type = $~[1]
641
+ is_collection = true
642
+ collection = []
643
+ end
641
644
 
642
- # Fill in the properties
645
+ klass_name = qualify_class_name(type.split('.')[-1])
646
+
647
+ if is_collection
648
+ # extract the elements from the collection
649
+ elements = complex_type_xml.xpath(".//d:element", @namespaces)
650
+ elements.each do |e|
651
+ if type.match(/^Edm/)
652
+ collection << parse_value(e.content, type)
653
+ else
654
+ element = @classes[klass_name].new
655
+ fill_complex_type_properties(e, element)
656
+ collection << element
657
+ end
658
+ end
659
+ return collection
660
+ else
661
+ klass = @classes[klass_name].new
662
+ # Fill in the properties
663
+ fill_complex_type_properties(complex_type_xml, klass)
664
+ return klass
665
+ end
666
+ end
667
+
668
+ # Helper method for complex_type_to_class
669
+ def fill_complex_type_properties(complex_type_xml, klass)
643
670
  properties = complex_type_xml.xpath(".//*")
644
671
  properties.each do |prop|
645
- klass.send "#{prop.name}=", parse_value(prop)
672
+ klass.send "#{prop.name}=", parse_value_xml(prop)
646
673
  end
647
-
648
- return klass
649
674
  end
650
675
 
651
676
  # Field Converters
@@ -668,31 +693,36 @@ class Service
668
693
  end
669
694
 
670
695
  # Parses a value into the proper type based on an xml property element
671
- def parse_value(property_xml)
696
+ def parse_value_xml(property_xml)
672
697
  property_type = Helpers.get_namespaced_attribute(property_xml, 'type', 'm')
673
698
  property_null = Helpers.get_namespaced_attribute(property_xml, 'null', 'm')
674
699
 
675
- # Handle a nil property type, this is a string
676
- return property_xml.content if property_type.nil?
700
+ if property_type.nil? || (property_type && property_type.match(/^Edm/))
701
+ return parse_value(property_xml.content, property_type, property_null)
702
+ end
703
+
704
+ complex_type_to_class(property_xml)
705
+ end
677
706
 
707
+ def parse_value(content, property_type = nil, property_null = nil)
678
708
  # Handle anything marked as null
679
709
  return nil if !property_null.nil? && property_null == "true"
680
710
 
681
- # Handle complex types
682
- return complex_type_to_class(property_xml) if !property_type.match(/^Edm/)
711
+ # Handle a nil property type, this is a string
712
+ return content if property_type.nil?
683
713
 
684
714
  # Handle integers
685
- return property_xml.content.to_i if property_type.match(/^Edm.Int/)
715
+ return content.to_i if property_type.match(/^Edm.Int/)
686
716
 
687
717
  # Handle decimals
688
- return property_xml.content.to_d if property_type.match(/Edm.Decimal/)
718
+ return content.to_d if property_type.match(/Edm.Decimal/)
689
719
 
690
720
  # Handle DateTimes
691
721
  # return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/)
692
- return parse_date(property_xml.content) if property_type.match(/Edm.DateTime/)
722
+ return parse_date(content) if property_type.match(/Edm.DateTime/)
693
723
 
694
724
  # If we can't parse the value, just return the element's content
695
- property_xml.content
725
+ content
696
726
  end
697
727
 
698
728
  # Parses a value into the proper type based on a specified return type