ruby_odata 0.0.6 → 0.0.7
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.
- 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
|