ruby_odata 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +11 -5
- data/VERSION +1 -1
- data/features/complex_types.feature +52 -0
- data/features/service.feature +5 -5
- data/features/step_definitions/service_steps.rb +101 -52
- data/features/type_conversion.feature +45 -0
- data/lib/ruby_odata.rb +2 -0
- data/lib/ruby_odata/class_builder.rb +21 -5
- data/lib/ruby_odata/service.rb +70 -8
- data/ruby_odata.gemspec +6 -2
- data/test/SampleService/App_Code/AuditFields.cs +13 -0
- data/test/SampleService/App_Code/Model.Designer.cs +141 -1
- data/test/SampleService/App_Code/Model.edmx +16 -2
- data/test/SampleService/App_Code/ModelContainerExtended.cs +32 -0
- data/test/SampleService/App_Data/_TestDB.mdf +0 -0
- data/test/SampleService/App_Data/_TestDB_Log.ldf +0 -0
- data/test/SampleService/web.config +1 -1
- data/test/blueprints.rb +5 -0
- metadata +8 -4
data/CHANGELOG.rdoc
CHANGED
@@ -23,10 +23,16 @@
|
|
23
23
|
|
24
24
|
=== 0.0.5
|
25
25
|
* Bug Fixes
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
* Works with Ruby 1.9.1
|
27
|
+
* Works with ActiveSupport 3.0.0.beta4
|
28
|
+
|
29
29
|
=== 0.0.6
|
30
30
|
* New Features
|
31
|
-
|
32
|
-
|
31
|
+
* Ability to batch saves (Adds, Updates, Deletes); this will help save on network chatter
|
32
|
+
|
33
|
+
=== 0.0.7
|
34
|
+
* New Features
|
35
|
+
* Complex Types are now supported
|
36
|
+
* Support for Edm.Int16, Edm.Int32, Edm.Int64
|
37
|
+
* Support for Edm.Decimal
|
38
|
+
* Support for Edm.DateTime
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.7
|
@@ -0,0 +1,52 @@
|
|
1
|
+
Feature: Complex types
|
2
|
+
In order to fully support OData services
|
3
|
+
As a user of ruby_odata
|
4
|
+
I want to be able to manage objects with complex types
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
|
8
|
+
And blueprints exist for the service
|
9
|
+
|
10
|
+
Scenario: The proxy must generate classes for complex types if they exist
|
11
|
+
Then a class named "AuditFields" should exist
|
12
|
+
|
13
|
+
Scenario: Complex properties on an entity must be the correct type
|
14
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
15
|
+
And I save changes
|
16
|
+
And I call "Products" on the service with args: "1"
|
17
|
+
When I run the query
|
18
|
+
Then the result should have a method: "AuditFields"
|
19
|
+
And the method "AuditFields" on the result should be of type "AuditFields"
|
20
|
+
|
21
|
+
Scenario: Complex properties on an entity must be filled
|
22
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
23
|
+
And I save changes
|
24
|
+
And I call "Products" on the service with args: "1"
|
25
|
+
When I run the query
|
26
|
+
Then the result should have a method: "AuditFields"
|
27
|
+
When I call "CreateDate" for "AuditFields" on the result
|
28
|
+
Then the operation should not be null
|
29
|
+
|
30
|
+
# TODO: This scenario should have the AuditFields.CreatedBy field set in the Given
|
31
|
+
# instead it is set by the blueprint
|
32
|
+
Scenario: Complex properties should be able to be added
|
33
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
34
|
+
And I save changes
|
35
|
+
And I call "Products" on the service with args: "1"
|
36
|
+
When I run the query
|
37
|
+
Then the method "CreatedBy" on the result's method "AuditFields" should equal: "Cucumber"
|
38
|
+
|
39
|
+
Scenario: Complex propertis should be able to be updated
|
40
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
41
|
+
And I save changes
|
42
|
+
And I call "Products" on the service with args: "1"
|
43
|
+
When I run the query
|
44
|
+
When I set "CreatedBy" on the result's method "AuditFields" to "This Test"
|
45
|
+
And I call "update_object" on the service with the last query result
|
46
|
+
And I save changes
|
47
|
+
Then the save result should equal: "true"
|
48
|
+
When I call "Products" on the service with args: "1"
|
49
|
+
And I run the query
|
50
|
+
Then the method "CreatedBy" on the result's method "AuditFields" should equal: "This Test"
|
51
|
+
|
52
|
+
|
data/features/service.feature
CHANGED
@@ -5,7 +5,7 @@ Feature: Service Should Generate a Proxy
|
|
5
5
|
|
6
6
|
Background:
|
7
7
|
Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
|
8
|
-
|
8
|
+
And blueprints exist for the service
|
9
9
|
|
10
10
|
Scenario: Service should respond to valid collections
|
11
11
|
Then I should be able to call "Categories" on the service
|
@@ -18,14 +18,14 @@ Scenario: Service should respond to accessing a single entity by ID
|
|
18
18
|
|
19
19
|
Scenario: Access an entity by ID should return the entity type
|
20
20
|
Given I call "AddToCategories" on the service with a new "Category" object with Name: "Test Category"
|
21
|
-
|
21
|
+
And I save changes
|
22
22
|
And I call "Categories" on the service with args: "1"
|
23
23
|
When I run the query
|
24
24
|
Then the result should be of type "Category"
|
25
25
|
|
26
26
|
Scenario: Entity should have the correct accessors
|
27
27
|
Given I call "AddToCategories" on the service with a new "Category" object with Name: "Test Category"
|
28
|
-
|
28
|
+
And I save changes
|
29
29
|
And I call "Categories" on the service with args: "1"
|
30
30
|
When I run the query
|
31
31
|
Then the result should have a method: "Id"
|
@@ -33,7 +33,7 @@ Scenario: Entity should have the correct accessors
|
|
33
33
|
|
34
34
|
Scenario: Entity should fill values
|
35
35
|
Given I call "AddToCategories" on the service with a new "Category" object with Name: "Test Category"
|
36
|
-
|
36
|
+
And I save changes
|
37
37
|
And I call "Categories" on the service with args: "1"
|
38
38
|
When I run the query
|
39
39
|
Then the method "Id" on the result should equal: "1"
|
@@ -41,7 +41,7 @@ Scenario: Entity should fill values
|
|
41
41
|
|
42
42
|
Scenario: Navigation Properties should be included in results
|
43
43
|
Given I call "AddToProducts" on the service with a new "Product" object
|
44
|
-
|
44
|
+
And I save changes
|
45
45
|
And I call "Products" on the service with args: "1"
|
46
46
|
When I run the query
|
47
47
|
Then the result should have a method: "Category"
|
@@ -39,7 +39,7 @@ Then /^the result should have a method: "([^\"]*)"$/ do |method|
|
|
39
39
|
end
|
40
40
|
|
41
41
|
Then /^the method "([^\"]*)" on the result should equal: "([^\"]*)"$/ do |method, value|
|
42
|
-
@service_result.send(method.to_sym).should == value
|
42
|
+
@service_result.send(method.to_sym).to_s.should == value
|
43
43
|
end
|
44
44
|
|
45
45
|
Then /^the method "([^\"]*)" on the result should be nil$/ do |method|
|
@@ -47,7 +47,7 @@ Then /^the method "([^\"]*)" on the result should be nil$/ do |method|
|
|
47
47
|
end
|
48
48
|
|
49
49
|
When /^I set "([^\"]*)" on the result to "([^\"]*)"$/ do |property_name, value|
|
50
|
-
|
50
|
+
@service_result.send("#{property_name}=", value)
|
51
51
|
end
|
52
52
|
|
53
53
|
Given /^I expand the query to include "([^\"]*)"$/ do |expands|
|
@@ -67,7 +67,7 @@ When /^I skip (\d+)$/ do |skip|
|
|
67
67
|
end
|
68
68
|
|
69
69
|
When /^I ask for the top (\d+)$/ do |top|
|
70
|
-
|
70
|
+
@service_query.top(top)
|
71
71
|
end
|
72
72
|
|
73
73
|
Then /^the method "([^\"]*)" on the result should be of type "([^\"]*)"$/ do |method, type|
|
@@ -79,18 +79,17 @@ Given /^I call "([^\"]*)" on the service with a new "([^\"]*)" object(?: with (.
|
|
79
79
|
fields_hash = {}
|
80
80
|
|
81
81
|
if !fields.nil?
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
end
|
82
|
+
fields.split(', ').each do |field|
|
83
|
+
if field =~ /^(?:(\w+): "(.*)")$/
|
84
|
+
key = $1
|
85
|
+
val = $2
|
86
|
+
if val =~ /^@@LastSave$/
|
87
|
+
val = @saved_result
|
88
|
+
end
|
89
|
+
|
90
|
+
fields_hash.merge!({ key => val })
|
91
|
+
end
|
92
|
+
end
|
94
93
|
end
|
95
94
|
|
96
95
|
obj = object.constantize.send(:make, fields_hash)
|
@@ -102,7 +101,7 @@ When /^I save changes$/ do
|
|
102
101
|
end
|
103
102
|
|
104
103
|
Then /^the save result should be of type "([^\"]*)"$/ do |type|
|
105
|
-
|
104
|
+
@saved_result.class.to_s.should == type
|
106
105
|
end
|
107
106
|
|
108
107
|
When /^I call "([^\"]*)" on the service with the last save result$/ do |method|
|
@@ -119,33 +118,42 @@ end
|
|
119
118
|
|
120
119
|
Then /^the method "([^\"]*)" on the save result should equal: "([^\"]*)"$/ do |method, value|
|
121
120
|
result = @saved_result.send(method.to_sym)
|
122
|
-
|
121
|
+
result.should == value
|
123
122
|
end
|
124
123
|
|
125
124
|
When /^blueprints exist for the service$/ do
|
126
|
-
|
125
|
+
require File.expand_path(File.dirname(__FILE__) + "../../../test/blueprints")
|
127
126
|
end
|
128
127
|
|
129
128
|
Given /^I call "([^\"]*)" on the service with a new "([^\"]*)" object it should throw an exception with message "([^\"]*)"$/ do |method, object, msg|
|
130
|
-
|
129
|
+
obj = object.constantize.send :make
|
131
130
|
lambda { @service.send(method.to_sym, obj) }.should raise_error(msg)
|
132
131
|
end
|
133
132
|
|
134
133
|
Then /^no "([^\"]*)" should exist$/ do |collection|
|
135
|
-
|
136
|
-
|
137
|
-
|
134
|
+
@service.send(collection)
|
135
|
+
results = @service.execute
|
136
|
+
results.should == []
|
138
137
|
end
|
139
138
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
139
|
+
Given /^the following (.*) exist:$/ do |plural_factory, table|
|
140
|
+
# table is a Cucumber::Ast::Table
|
141
|
+
factory = plural_factory.singularize
|
142
|
+
table.hashes.map do |hash|
|
143
|
+
obj = factory.constantize.send(:make, hash)
|
144
|
+
@service.send("AddTo#{plural_factory}", obj)
|
145
|
+
@service.save_changes
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
Given /^(\d+) (.*) exist$/ do |num, plural_factory|
|
150
|
+
factory = plural_factory.singularize
|
151
|
+
num.to_i.times do
|
152
|
+
obj = factory.constantize.send(:make)
|
153
|
+
@service.send("AddTo#{plural_factory}", obj)
|
154
|
+
end
|
155
|
+
@service.save_changes # Batch save
|
156
|
+
end
|
149
157
|
|
150
158
|
Then /^the result should be:$/ do |table|
|
151
159
|
# table is a Cucumber::Ast::Table
|
@@ -156,16 +164,16 @@ Then /^the result should be:$/ do |table|
|
|
156
164
|
results = []
|
157
165
|
|
158
166
|
@service_result.each do |result|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
167
|
+
obj_hash = Hash.new
|
168
|
+
fields.each do |field|
|
169
|
+
obj_hash[field] = result.send(field)
|
170
|
+
end
|
171
|
+
results << obj_hash
|
172
|
+
end
|
173
|
+
|
174
|
+
result_table = Cucumber::Ast::Table.new(results)
|
175
|
+
|
176
|
+
table.diff!(result_table)
|
169
177
|
end
|
170
178
|
|
171
179
|
Then /^the save result should be:$/ do |table|
|
@@ -177,19 +185,60 @@ Then /^the save result should be:$/ do |table|
|
|
177
185
|
results = []
|
178
186
|
|
179
187
|
@saved_result.each do |result|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
188
|
+
obj_hash = Hash.new
|
189
|
+
fields.each do |field|
|
190
|
+
obj_hash[field] = result.send(field)
|
191
|
+
end
|
192
|
+
results << obj_hash
|
193
|
+
end
|
194
|
+
|
195
|
+
result_table = Cucumber::Ast::Table.new(results)
|
196
|
+
|
197
|
+
table.diff!(result_table)
|
190
198
|
end
|
191
199
|
|
200
|
+
Then /^a class named "([^\"]*)" should exist$/ do |klass_name|
|
201
|
+
(Object.const_defined? klass_name).should == true
|
202
|
+
end
|
203
|
+
|
204
|
+
# Operations against a method on the service result
|
205
|
+
When /^I call "([^\"]*)" for "([^\"]*)" on the result$/ do |method2, method1|
|
206
|
+
r1 = @service_result.send(method1)
|
207
|
+
@operation_result = r1.send(method2)
|
208
|
+
end
|
209
|
+
|
210
|
+
Then /^the operation should not be null$/ do
|
211
|
+
@operation_result.nil?.should == false
|
212
|
+
end
|
213
|
+
|
214
|
+
|
192
215
|
Then /^the method "([^\"]*)" on the result's method "([^\"]*)" should equal: "([^\"]*)"$/ do |method, result_method, value|
|
193
216
|
obj = @service_result.send(result_method.to_sym)
|
194
|
-
obj.send(method.to_sym).should == value
|
217
|
+
obj.send(method.to_sym).to_s.should == value
|
218
|
+
end
|
219
|
+
|
220
|
+
When /^I set "([^\"]*)" on the result's method "([^\"]*)" to "([^\"]*)"$/ do |property_name, result_method, value|
|
221
|
+
@service_result.send(result_method).send("#{property_name}=", value)
|
195
222
|
end
|
223
|
+
|
224
|
+
# Type tests
|
225
|
+
Then /^the "([^\"]*)" method should return a (.*)/ do |method_name, type|
|
226
|
+
methods = method_name.split '.'
|
227
|
+
if methods.length == 1
|
228
|
+
@service_result.send(method_name).class.to_s.should == type
|
229
|
+
else
|
230
|
+
@service_result.send(methods[0]).send(methods[1]).class.to_s.should == type
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
Then /^I store the last query result for comparison$/ do
|
235
|
+
@stored_query_result = @service_result
|
236
|
+
end
|
237
|
+
Then /^the new query result's time "([^\"]*)" should equal the saved query result$/ do |method_name|
|
238
|
+
methods = method_name.split '.'
|
239
|
+
if methods.length == 1
|
240
|
+
@service_result.send(method_name).xmlschema(3).should == @stored_query_result.send(method_name).xmlschema(3)
|
241
|
+
else
|
242
|
+
@service_result.send(methods[0]).send(methods[1]).xmlschema(3).should == @stored_query_result.send(methods[0]).send(methods[1]).xmlschema(3)
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Feature: Type conversion
|
2
|
+
In order to accurately perform operations
|
3
|
+
As a user of the API
|
4
|
+
I want types returned to be accurately represented
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
|
8
|
+
And blueprints exist for the service
|
9
|
+
|
10
|
+
Scenario: Integers should be Fixnums
|
11
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
12
|
+
And I save changes
|
13
|
+
When I call "Products" on the service
|
14
|
+
And I run the query
|
15
|
+
Then the "Id" method should return a Fixnum
|
16
|
+
|
17
|
+
Scenario: Decimals should be BigDecimals
|
18
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
19
|
+
And I save changes
|
20
|
+
When I call "Products" on the service
|
21
|
+
And I run the query
|
22
|
+
Then the "Price" method should return a BigDecimal
|
23
|
+
|
24
|
+
Scenario: DateTimes should be Times
|
25
|
+
Given I call "AddToProducts" on the service with a new "Product" object
|
26
|
+
And I save changes
|
27
|
+
When I call "Products" on the service
|
28
|
+
And I run the query
|
29
|
+
Then the "AuditFields.CreateDate" method should return a Time
|
30
|
+
|
31
|
+
Scenario: Verify that DateTimes don't change if not modified on an update
|
32
|
+
Given I call "AddToProducts" on the service with a new "Product" object with Name: "Test Product"
|
33
|
+
When I save changes
|
34
|
+
And I call "Products" on the service with args: "1"
|
35
|
+
And I run the query
|
36
|
+
Then I store the last query result for comparison
|
37
|
+
When I set "Name" on the result to "Changed Test Product"
|
38
|
+
Then the method "Name" on the result should equal: "Changed Test Product"
|
39
|
+
And I call "update_object" on the service with the last query result
|
40
|
+
And I save changes
|
41
|
+
Then the save result should equal: "true"
|
42
|
+
When I call "Products" on the service with args: "1"
|
43
|
+
And I run the query
|
44
|
+
Then the new query result's time "AuditFields.CreateDate" should equal the saved query result
|
45
|
+
|
data/lib/ruby_odata.rb
CHANGED
@@ -64,10 +64,26 @@ module OData
|
|
64
64
|
vars.delete(meta)
|
65
65
|
end
|
66
66
|
|
67
|
+
# Convert a BigDecimal to a string for serialization (to match Edm.Decimal)
|
68
|
+
decimals = vars.find_all { |o| o[1].class == BigDecimal } || []
|
69
|
+
decimals.each do |d|
|
70
|
+
vars[d[0]] = d[1].to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
# Convert Time to an RFC3339 string for serialization
|
74
|
+
times = vars.find_all { |o| o[1].class == Time } || []
|
75
|
+
times.each do |t|
|
76
|
+
sdate = t[1].xmlschema(3)
|
77
|
+
# Remove the ending Z (indicating UTC).
|
78
|
+
# If the Z is there when saving, the time is converted to local time on the server
|
79
|
+
sdate.chop! if sdate.match(/Z$/)
|
80
|
+
vars[t[0]] = sdate
|
81
|
+
end
|
82
|
+
|
67
83
|
vars
|
68
84
|
end
|
69
|
-
|
70
|
-
|
85
|
+
|
86
|
+
|
71
87
|
# Add the methods that were passed in
|
72
88
|
@methods.each do |method_name|
|
73
89
|
klass.send :define_method, method_name do
|
@@ -78,7 +94,7 @@ module OData
|
|
78
94
|
end
|
79
95
|
end
|
80
96
|
end
|
81
|
-
|
97
|
+
|
82
98
|
def add_nav_props(klass)
|
83
99
|
@nav_props.each do |method_name|
|
84
100
|
klass.send :define_method, method_name do
|
@@ -86,8 +102,8 @@ module OData
|
|
86
102
|
end
|
87
103
|
klass.send :define_method, "#{method_name}=" do |value|
|
88
104
|
instance_variable_set("@#{method_name}", value)
|
89
|
-
end
|
105
|
+
end
|
90
106
|
end
|
91
107
|
end
|
92
108
|
end
|
93
|
-
end # module OData
|
109
|
+
end # module OData
|
data/lib/ruby_odata/service.rb
CHANGED
@@ -109,12 +109,15 @@ class Service
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
-
private
|
112
|
+
private
|
113
|
+
# Retrieves collections from the main service page
|
113
114
|
def get_collections
|
114
115
|
doc = Nokogiri::XML(open(@uri))
|
115
116
|
collections = doc.xpath("//app:collection", "app" => "http://www.w3.org/2007/app")
|
116
117
|
collections.collect { |c| c["href"] }
|
117
|
-
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Build the classes required by the metadata
|
118
121
|
def build_classes
|
119
122
|
@classes = Hash.new
|
120
123
|
doc = Nokogiri::XML(open("#{@uri}/$metadata"))
|
@@ -122,6 +125,15 @@ class Service
|
|
122
125
|
# Get the edm namespace
|
123
126
|
edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s
|
124
127
|
|
128
|
+
# Build complex types first, these will be used for entities
|
129
|
+
complex_types = doc.xpath("//edm:ComplexType", "edm" => edm_ns) || []
|
130
|
+
complex_types.each do |c|
|
131
|
+
name = c['Name']
|
132
|
+
props = c.xpath(".//edm:Property", "edm" => edm_ns)
|
133
|
+
methods = props.collect { |p| p['Name'] } # Standard Properties
|
134
|
+
@classes[name] = ClassBuilder.new(name, methods, []).build unless @classes.keys.include?(name)
|
135
|
+
end
|
136
|
+
|
125
137
|
entity_types = doc.xpath("//edm:EntityType", "edm" => edm_ns)
|
126
138
|
entity_types.each do |e|
|
127
139
|
name = e['Name']
|
@@ -132,6 +144,8 @@ class Service
|
|
132
144
|
@classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name)
|
133
145
|
end
|
134
146
|
end
|
147
|
+
|
148
|
+
# Helper to loop through a result and create an instance for each entity in the results
|
135
149
|
def build_classes_from_result(result)
|
136
150
|
doc = Nokogiri::XML(result)
|
137
151
|
entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom")
|
@@ -143,6 +157,8 @@ class Service
|
|
143
157
|
end
|
144
158
|
return results
|
145
159
|
end
|
160
|
+
|
161
|
+
# Converts an XML Entry into a class
|
146
162
|
def entry_to_class(entry)
|
147
163
|
# Retrieve the class name from the fully qualified name (the last string after the last dot)
|
148
164
|
klass_name = entry.xpath("./atom:category/@term", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1]
|
@@ -159,8 +175,7 @@ class Service
|
|
159
175
|
# Fill properties
|
160
176
|
for prop in properties
|
161
177
|
prop_name = prop.name
|
162
|
-
|
163
|
-
klass.send "#{prop_name}=", prop.content
|
178
|
+
klass.send "#{prop_name}=", parse_value(prop)
|
164
179
|
end
|
165
180
|
|
166
181
|
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" })
|
@@ -188,6 +203,7 @@ class Service
|
|
188
203
|
|
189
204
|
return klass
|
190
205
|
end
|
206
|
+
|
191
207
|
def build_query_uri
|
192
208
|
"#{@uri}#{@query.query}"
|
193
209
|
end
|
@@ -215,6 +231,11 @@ class Service
|
|
215
231
|
return (delete_result.code == 204)
|
216
232
|
end
|
217
233
|
end
|
234
|
+
|
235
|
+
# Batch Saves
|
236
|
+
def generate_guid
|
237
|
+
rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
|
238
|
+
end
|
218
239
|
def batch_save(operations)
|
219
240
|
batch_num = generate_guid
|
220
241
|
changeset_num = generate_guid
|
@@ -245,7 +266,6 @@ class Service
|
|
245
266
|
|
246
267
|
return body
|
247
268
|
end
|
248
|
-
|
249
269
|
def build_batch_operation(operation, changeset_num)
|
250
270
|
accept_headers = "Accept-Charset: utf-8\n"
|
251
271
|
accept_headers << "Content-Type: application/json;charset=utf-8\n" unless operation.kind == "Delete"
|
@@ -278,10 +298,52 @@ class Service
|
|
278
298
|
|
279
299
|
return content
|
280
300
|
end
|
281
|
-
|
282
|
-
|
283
|
-
|
301
|
+
|
302
|
+
# Complex Types
|
303
|
+
def complex_type_to_class(complex_type_xml)
|
304
|
+
klass_name = complex_type_xml.attr('type').split('.')[-1]
|
305
|
+
klass = @classes[klass_name].new
|
306
|
+
|
307
|
+
# Fill in the properties
|
308
|
+
properties = complex_type_xml.xpath(".//*")
|
309
|
+
properties.each do |prop|
|
310
|
+
klass.send "#{prop.name}=", parse_value(prop)
|
311
|
+
end
|
312
|
+
|
313
|
+
return klass
|
284
314
|
end
|
315
|
+
|
316
|
+
# Field Converters
|
317
|
+
def parse_value(property_xml)
|
318
|
+
property_type = property_xml.attr('type')
|
319
|
+
|
320
|
+
# Handle a nil property type, this is a string
|
321
|
+
return property_xml.content if property_type.nil?
|
322
|
+
|
323
|
+
# Handle complex types
|
324
|
+
return complex_type_to_class(property_xml) if !property_type.match(/^Edm/)
|
325
|
+
|
326
|
+
# Handle integers
|
327
|
+
return property_xml.content.to_i if property_type.match(/^Edm.Int/)
|
328
|
+
|
329
|
+
# Handle decimals
|
330
|
+
return property_xml.content.to_d if property_type.match(/Edm.Decimal/)
|
331
|
+
|
332
|
+
# Handle DateTimes
|
333
|
+
# return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/)
|
334
|
+
if property_type.match(/Edm.DateTime/)
|
335
|
+
sdate = property_xml.content
|
336
|
+
|
337
|
+
# Assume this is UTC if no timezone is specified
|
338
|
+
sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
|
339
|
+
|
340
|
+
return Time.parse(sdate)
|
341
|
+
end
|
342
|
+
|
343
|
+
# If we can't parse the value, just return the element's content
|
344
|
+
property_xml.content
|
345
|
+
end
|
346
|
+
|
285
347
|
end
|
286
348
|
|
287
349
|
end # module OData
|
data/ruby_odata.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{ruby_odata}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.7"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Damien White"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-07-04}
|
13
13
|
s.description = %q{An OData Client Library for Ruby. Use this to interact with OData services}
|
14
14
|
s.email = %q{damien.white@visoftinc.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -26,12 +26,14 @@ Gem::Specification.new do |s|
|
|
26
26
|
"VERSION",
|
27
27
|
"config/cucumber.yml",
|
28
28
|
"features/batch_request.feature",
|
29
|
+
"features/complex_types.feature",
|
29
30
|
"features/query_builder.feature",
|
30
31
|
"features/service.feature",
|
31
32
|
"features/service_manage.feature",
|
32
33
|
"features/step_definitions/service_steps.rb",
|
33
34
|
"features/support/env.rb",
|
34
35
|
"features/support/hooks.rb",
|
36
|
+
"features/type_conversion.feature",
|
35
37
|
"lib/ruby_odata.rb",
|
36
38
|
"lib/ruby_odata/class_builder.rb",
|
37
39
|
"lib/ruby_odata/operation.rb",
|
@@ -40,9 +42,11 @@ Gem::Specification.new do |s|
|
|
40
42
|
"ruby_odata.gemspec",
|
41
43
|
"test/Cassini x64.bat",
|
42
44
|
"test/Cassini x86.bat",
|
45
|
+
"test/SampleService/App_Code/AuditFields.cs",
|
43
46
|
"test/SampleService/App_Code/Entities.cs",
|
44
47
|
"test/SampleService/App_Code/Model.Designer.cs",
|
45
48
|
"test/SampleService/App_Code/Model.edmx",
|
49
|
+
"test/SampleService/App_Code/ModelContainerExtended.cs",
|
46
50
|
"test/SampleService/App_Data/_TestDB.mdf",
|
47
51
|
"test/SampleService/App_Data/_TestDB_Log.ldf",
|
48
52
|
"test/SampleService/Entities.svc",
|
@@ -252,13 +252,15 @@ namespace Model
|
|
252
252
|
/// <param name="name">Initial value of the Name property.</param>
|
253
253
|
/// <param name="description">Initial value of the Description property.</param>
|
254
254
|
/// <param name="price">Initial value of the Price property.</param>
|
255
|
-
|
255
|
+
/// <param name="auditFields">Initial value of the AuditFields property.</param>
|
256
|
+
public static Product CreateProduct(global::System.Int32 id, global::System.String name, global::System.String description, global::System.Decimal price, AuditFields auditFields)
|
256
257
|
{
|
257
258
|
Product product = new Product();
|
258
259
|
product.Id = id;
|
259
260
|
product.Name = name;
|
260
261
|
product.Description = description;
|
261
262
|
product.Price = price;
|
263
|
+
product.AuditFields = StructuralObject.VerifyComplexObjectIsNotNull(auditFields, "AuditFields");
|
262
264
|
return product;
|
263
265
|
}
|
264
266
|
|
@@ -364,6 +366,40 @@ namespace Model
|
|
364
366
|
partial void OnPriceChanging(global::System.Decimal value);
|
365
367
|
partial void OnPriceChanged();
|
366
368
|
|
369
|
+
#endregion
|
370
|
+
#region Complex Properties
|
371
|
+
|
372
|
+
/// <summary>
|
373
|
+
/// No Metadata Documentation available.
|
374
|
+
/// </summary>
|
375
|
+
[EdmComplexPropertyAttribute()]
|
376
|
+
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
|
377
|
+
[XmlElement(IsNullable=true)]
|
378
|
+
[SoapElement(IsNullable=true)]
|
379
|
+
[DataMemberAttribute()]
|
380
|
+
public AuditFields AuditFields
|
381
|
+
{
|
382
|
+
get
|
383
|
+
{
|
384
|
+
_AuditFields = GetValidValue(_AuditFields, "AuditFields", false, _AuditFieldsInitialized);
|
385
|
+
_AuditFieldsInitialized = true;
|
386
|
+
return _AuditFields;
|
387
|
+
}
|
388
|
+
set
|
389
|
+
{
|
390
|
+
OnAuditFieldsChanging(value);
|
391
|
+
ReportPropertyChanging("AuditFields");
|
392
|
+
_AuditFields = SetValidValue(_AuditFields, value, "AuditFields");
|
393
|
+
_AuditFieldsInitialized = true;
|
394
|
+
ReportPropertyChanged("AuditFields");
|
395
|
+
OnAuditFieldsChanged();
|
396
|
+
}
|
397
|
+
}
|
398
|
+
private AuditFields _AuditFields;
|
399
|
+
private bool _AuditFieldsInitialized;
|
400
|
+
partial void OnAuditFieldsChanging(AuditFields value);
|
401
|
+
partial void OnAuditFieldsChanged();
|
402
|
+
|
367
403
|
#endregion
|
368
404
|
|
369
405
|
#region Navigation Properties
|
@@ -410,5 +446,109 @@ namespace Model
|
|
410
446
|
}
|
411
447
|
|
412
448
|
#endregion
|
449
|
+
#region ComplexTypes
|
450
|
+
|
451
|
+
/// <summary>
|
452
|
+
/// No Metadata Documentation available.
|
453
|
+
/// </summary>
|
454
|
+
[EdmComplexTypeAttribute(NamespaceName="Model", Name="AuditFields")]
|
455
|
+
[DataContractAttribute(IsReference=true)]
|
456
|
+
[Serializable()]
|
457
|
+
public partial class AuditFields : ComplexObject
|
458
|
+
{
|
459
|
+
#region Factory Method
|
460
|
+
|
461
|
+
/// <summary>
|
462
|
+
/// Create a new AuditFields object.
|
463
|
+
/// </summary>
|
464
|
+
/// <param name="createDate">Initial value of the CreateDate property.</param>
|
465
|
+
/// <param name="modifiedDate">Initial value of the ModifiedDate property.</param>
|
466
|
+
public static AuditFields CreateAuditFields(global::System.DateTime createDate, global::System.DateTime modifiedDate)
|
467
|
+
{
|
468
|
+
AuditFields auditFields = new AuditFields();
|
469
|
+
auditFields.CreateDate = createDate;
|
470
|
+
auditFields.ModifiedDate = modifiedDate;
|
471
|
+
return auditFields;
|
472
|
+
}
|
473
|
+
|
474
|
+
#endregion
|
475
|
+
#region Primitive Properties
|
476
|
+
|
477
|
+
/// <summary>
|
478
|
+
/// No Metadata Documentation available.
|
479
|
+
/// </summary>
|
480
|
+
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
|
481
|
+
[DataMemberAttribute()]
|
482
|
+
public global::System.DateTime CreateDate
|
483
|
+
{
|
484
|
+
get
|
485
|
+
{
|
486
|
+
return _CreateDate;
|
487
|
+
}
|
488
|
+
set
|
489
|
+
{
|
490
|
+
OnCreateDateChanging(value);
|
491
|
+
ReportPropertyChanging("CreateDate");
|
492
|
+
_CreateDate = StructuralObject.SetValidValue(value);
|
493
|
+
ReportPropertyChanged("CreateDate");
|
494
|
+
OnCreateDateChanged();
|
495
|
+
}
|
496
|
+
}
|
497
|
+
private global::System.DateTime _CreateDate;
|
498
|
+
partial void OnCreateDateChanging(global::System.DateTime value);
|
499
|
+
partial void OnCreateDateChanged();
|
500
|
+
|
501
|
+
/// <summary>
|
502
|
+
/// No Metadata Documentation available.
|
503
|
+
/// </summary>
|
504
|
+
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
|
505
|
+
[DataMemberAttribute()]
|
506
|
+
public global::System.DateTime ModifiedDate
|
507
|
+
{
|
508
|
+
get
|
509
|
+
{
|
510
|
+
return _ModifiedDate;
|
511
|
+
}
|
512
|
+
set
|
513
|
+
{
|
514
|
+
OnModifiedDateChanging(value);
|
515
|
+
ReportPropertyChanging("ModifiedDate");
|
516
|
+
_ModifiedDate = StructuralObject.SetValidValue(value);
|
517
|
+
ReportPropertyChanged("ModifiedDate");
|
518
|
+
OnModifiedDateChanged();
|
519
|
+
}
|
520
|
+
}
|
521
|
+
private global::System.DateTime _ModifiedDate;
|
522
|
+
partial void OnModifiedDateChanging(global::System.DateTime value);
|
523
|
+
partial void OnModifiedDateChanged();
|
524
|
+
|
525
|
+
/// <summary>
|
526
|
+
/// No Metadata Documentation available.
|
527
|
+
/// </summary>
|
528
|
+
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
|
529
|
+
[DataMemberAttribute()]
|
530
|
+
public global::System.String CreatedBy
|
531
|
+
{
|
532
|
+
get
|
533
|
+
{
|
534
|
+
return _CreatedBy;
|
535
|
+
}
|
536
|
+
set
|
537
|
+
{
|
538
|
+
OnCreatedByChanging(value);
|
539
|
+
ReportPropertyChanging("CreatedBy");
|
540
|
+
_CreatedBy = StructuralObject.SetValidValue(value, true);
|
541
|
+
ReportPropertyChanged("CreatedBy");
|
542
|
+
OnCreatedByChanged();
|
543
|
+
}
|
544
|
+
}
|
545
|
+
private global::System.String _CreatedBy;
|
546
|
+
partial void OnCreatedByChanging(global::System.String value);
|
547
|
+
partial void OnCreatedByChanged();
|
548
|
+
|
549
|
+
#endregion
|
550
|
+
}
|
551
|
+
|
552
|
+
#endregion
|
413
553
|
|
414
554
|
}
|
@@ -29,6 +29,9 @@
|
|
29
29
|
<Property Name="Description" Type="nvarchar(max)" Nullable="false" />
|
30
30
|
<Property Name="Price" Type="money" Nullable="false" />
|
31
31
|
<Property Name="Category_Id" Type="int" Nullable="false" />
|
32
|
+
<Property Name="CreateDate" Type="datetime" Nullable="false" />
|
33
|
+
<Property Name="ModifiedDate" Type="datetime" Nullable="false" />
|
34
|
+
<Property Name="CreatedBy" Type="nvarchar" MaxLength="50" />
|
32
35
|
</EntityType>
|
33
36
|
<Association Name="FK_CategoryProduct">
|
34
37
|
<End Role="Categories" Type="Model.Store.Categories" Multiplicity="1" />
|
@@ -63,6 +66,7 @@
|
|
63
66
|
<Property Type="String" Name="Description" Nullable="false" />
|
64
67
|
<Property Type="Decimal" Name="Price" Nullable="false" />
|
65
68
|
<NavigationProperty Name="Category" Relationship="Model.CategoryProduct" FromRole="Product" ToRole="Category" />
|
69
|
+
<Property Name="AuditFields" Type="Model.AuditFields" Nullable="false" />
|
66
70
|
</EntityType>
|
67
71
|
<EntityType Name="Category">
|
68
72
|
<Key>
|
@@ -76,6 +80,11 @@
|
|
76
80
|
<End Type="Model.Category" Role="Category" Multiplicity="1" />
|
77
81
|
<End Type="Model.Product" Role="Product" Multiplicity="*" />
|
78
82
|
</Association>
|
83
|
+
<ComplexType Name="AuditFields">
|
84
|
+
<Property Type="DateTime" Name="CreateDate" Nullable="false" />
|
85
|
+
<Property Type="DateTime" Name="ModifiedDate" Nullable="false" />
|
86
|
+
<Property Type="String" Name="CreatedBy" Nullable="true" />
|
87
|
+
</ComplexType>
|
79
88
|
</Schema>
|
80
89
|
</edmx:ConceptualModels>
|
81
90
|
<!-- C-S mapping content -->
|
@@ -85,10 +94,15 @@
|
|
85
94
|
<EntitySetMapping Name="Products">
|
86
95
|
<EntityTypeMapping TypeName="IsTypeOf(Model.Product)">
|
87
96
|
<MappingFragment StoreEntitySet="Products">
|
88
|
-
|
97
|
+
<ScalarProperty Name="Id" ColumnName="Id" />
|
89
98
|
<ScalarProperty Name="Name" ColumnName="Name" />
|
90
99
|
<ScalarProperty Name="Description" ColumnName="Description" />
|
91
100
|
<ScalarProperty Name="Price" ColumnName="Price" />
|
101
|
+
<ComplexProperty Name="AuditFields">
|
102
|
+
<ScalarProperty Name="CreatedBy" ColumnName="CreatedBy" />
|
103
|
+
<ScalarProperty Name="ModifiedDate" ColumnName="ModifiedDate" />
|
104
|
+
<ScalarProperty Name="CreateDate" ColumnName="CreateDate" />
|
105
|
+
</ComplexProperty>
|
92
106
|
</MappingFragment>
|
93
107
|
</EntityTypeMapping>
|
94
108
|
</EntitySetMapping>
|
@@ -128,7 +142,7 @@
|
|
128
142
|
<!-- Diagram content (shape and connector positions) -->
|
129
143
|
<edmx:Diagrams>
|
130
144
|
<Diagram Name="Model" >
|
131
|
-
<EntityTypeShape EntityType="Model.Product" Width="1.5" PointX="8" PointY="1.75" Height="
|
145
|
+
<EntityTypeShape EntityType="Model.Product" Width="1.5" PointX="8" PointY="1.75" Height="2.172587890625" />
|
132
146
|
<EntityTypeShape EntityType="Model.Category" Width="1.5" PointX="5.25" PointY="1.875" Height="1.59568359375" />
|
133
147
|
<AssociationConnector Association="Model.CategoryProduct">
|
134
148
|
<ConnectorPoint PointX="6.75" PointY="2.672841796875" />
|
@@ -0,0 +1,32 @@
|
|
1
|
+
using System;
|
2
|
+
using System.Data;
|
3
|
+
using System.Data.Objects;
|
4
|
+
using System.Linq;
|
5
|
+
|
6
|
+
namespace Model
|
7
|
+
{
|
8
|
+
/// <summary>
|
9
|
+
/// Extensions to the default container
|
10
|
+
/// </summary>
|
11
|
+
public partial class ModelContainer
|
12
|
+
{
|
13
|
+
partial void OnContextCreated()
|
14
|
+
{
|
15
|
+
SavingChanges += ContainerSavingChanges;
|
16
|
+
}
|
17
|
+
|
18
|
+
private static void ContainerSavingChanges(object sender, EventArgs e)
|
19
|
+
{
|
20
|
+
var updatedEntites = ((ObjectContext) sender).ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
|
21
|
+
foreach (var ose in updatedEntites)
|
22
|
+
{
|
23
|
+
var type = ose.Entity.GetType();
|
24
|
+
if (type.GetProperties().Any(p => typeof(AuditFields).IsAssignableFrom(p.PropertyType)))
|
25
|
+
{
|
26
|
+
dynamic entity = ose.Entity;
|
27
|
+
entity.AuditFields.ModifiedDate = DateTime.UtcNow;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
Binary file
|
Binary file
|
@@ -5,7 +5,7 @@
|
|
5
5
|
-->
|
6
6
|
<configuration>
|
7
7
|
<system.web>
|
8
|
-
<compilation debug="
|
8
|
+
<compilation debug="true" targetFramework="4.0">
|
9
9
|
<assemblies>
|
10
10
|
<add assembly="System.Security, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
|
11
11
|
<add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
|
data/test/blueprints.rb
CHANGED
@@ -9,8 +9,13 @@ Product.blueprint do
|
|
9
9
|
Description "Test Widget"
|
10
10
|
Price { Sham.price }
|
11
11
|
Category { Category.make }
|
12
|
+
AuditFields { AuditFields.make }
|
12
13
|
end
|
13
14
|
|
14
15
|
Category.blueprint do
|
15
16
|
Name { Sham.category_name }
|
17
|
+
end
|
18
|
+
|
19
|
+
AuditFields.blueprint do
|
20
|
+
CreatedBy "Cucumber"
|
16
21
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_odata
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 7
|
10
|
+
version: 0.0.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Damien White
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-07-04 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -85,12 +85,14 @@ files:
|
|
85
85
|
- VERSION
|
86
86
|
- config/cucumber.yml
|
87
87
|
- features/batch_request.feature
|
88
|
+
- features/complex_types.feature
|
88
89
|
- features/query_builder.feature
|
89
90
|
- features/service.feature
|
90
91
|
- features/service_manage.feature
|
91
92
|
- features/step_definitions/service_steps.rb
|
92
93
|
- features/support/env.rb
|
93
94
|
- features/support/hooks.rb
|
95
|
+
- features/type_conversion.feature
|
94
96
|
- lib/ruby_odata.rb
|
95
97
|
- lib/ruby_odata/class_builder.rb
|
96
98
|
- lib/ruby_odata/operation.rb
|
@@ -99,9 +101,11 @@ files:
|
|
99
101
|
- ruby_odata.gemspec
|
100
102
|
- test/Cassini x64.bat
|
101
103
|
- test/Cassini x86.bat
|
104
|
+
- test/SampleService/App_Code/AuditFields.cs
|
102
105
|
- test/SampleService/App_Code/Entities.cs
|
103
106
|
- test/SampleService/App_Code/Model.Designer.cs
|
104
107
|
- test/SampleService/App_Code/Model.edmx
|
108
|
+
- test/SampleService/App_Code/ModelContainerExtended.cs
|
105
109
|
- test/SampleService/App_Data/_TestDB.mdf
|
106
110
|
- test/SampleService/App_Data/_TestDB_Log.ldf
|
107
111
|
- test/SampleService/Entities.svc
|