ruby_odata 0.1.3 → 0.1.4

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