safrano 0.4.2 → 0.5.0
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.
- checksums.yaml +4 -4
- data/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +140 -591
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +123 -172
- data/lib/odata/error.rb +183 -32
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +41 -25
- data/lib/odata/filter/sequel.rb +133 -62
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +106 -52
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +13 -26
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +23 -107
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -33
- data/lib/safrano/rack_app.rb +66 -65
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +96 -45
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +240 -130
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +32 -11
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
|
-
require_relative '../safrano/core
|
3
|
-
require_relative './entity
|
4
|
+
require_relative '../safrano/core'
|
5
|
+
require_relative './entity'
|
4
6
|
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# remove the relation between entity and parent by clearing
|
7
9
|
# the FK field(s) (if allowed)
|
8
10
|
def self.remove_nav_relation(assoc, parent)
|
@@ -73,29 +75,15 @@ module OData
|
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
76
|
-
module EntityBase
|
77
|
-
module NavigationInfo
|
78
|
-
attr_reader :nav_parent
|
79
|
-
attr_reader :navattr_reflection
|
80
|
-
attr_reader :nav_name
|
81
|
-
def set_relation_info(parent, name)
|
82
|
-
@nav_parent = parent
|
83
|
-
@nav_name = name
|
84
|
-
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
85
|
-
@nav_klass = @navattr_reflection[:class_name].constantize
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
78
|
# Represents a named but nil-valued navigation-attribute of an Entity
|
91
79
|
# (usually resulting from a NULL FK db value)
|
92
80
|
class NilNavigationAttribute
|
93
|
-
include
|
81
|
+
include Safrano::NavigationInfo
|
94
82
|
def odata_get(req)
|
95
83
|
if req.walker.media_value
|
96
|
-
|
84
|
+
Safrano::ErrorNotFound.odata_get
|
97
85
|
elsif req.accept?(APPJSON)
|
98
|
-
[200,
|
86
|
+
[200, EMPTY_HASH, to_odata_json(service: req.service)]
|
99
87
|
else # TODO: other formats
|
100
88
|
415
|
101
89
|
end
|
@@ -111,13 +99,10 @@ module OData
|
|
111
99
|
|
112
100
|
# create the nav. entity
|
113
101
|
def odata_put(req)
|
114
|
-
# if req.walker.raw_value
|
115
102
|
# delegate to the class method
|
116
103
|
@nav_klass.odata_create_entity_and_relation(req,
|
117
104
|
@navattr_reflection,
|
118
105
|
@nav_parent)
|
119
|
-
# else
|
120
|
-
# end
|
121
106
|
end
|
122
107
|
|
123
108
|
# empty output as OData json (v2)
|
@@ -133,16 +118,18 @@ module OData
|
|
133
118
|
# methods related to transitions to next state (cf. walker)
|
134
119
|
module Transitions
|
135
120
|
def transition_end(_match_result)
|
136
|
-
|
121
|
+
Safrano::Transition::RESULT_END
|
137
122
|
end
|
138
123
|
|
139
124
|
def transition_value(_match_result)
|
140
125
|
[self, :end_with_value]
|
141
126
|
end
|
142
127
|
|
128
|
+
ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
|
129
|
+
Safrano::TransitionValue].freeze
|
130
|
+
|
143
131
|
def allowed_transitions
|
144
|
-
|
145
|
-
Safrano::TransitionValue]
|
132
|
+
ALLOWED_TRANSITIONS
|
146
133
|
end
|
147
134
|
end
|
148
135
|
include Transitions
|
data/lib/odata/relations.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
4
|
|
5
5
|
# OData relation related classes/module
|
6
|
-
module
|
6
|
+
module Safrano
|
7
7
|
# we represent a relation as a Set (unordered) of two end elements
|
8
8
|
class Relation < Set
|
9
9
|
# attr_reader :rid
|
@@ -20,7 +20,7 @@ module OData
|
|
20
20
|
|
21
21
|
# we need a from/to order independant ID
|
22
22
|
def rid
|
23
|
-
|
23
|
+
Safrano::RelationManager.build_id(self)
|
24
24
|
end
|
25
25
|
|
26
26
|
# we need a from/to order independant OData like name
|
@@ -76,11 +76,11 @@ module OData
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def get(arg)
|
79
|
-
rid =
|
79
|
+
rid = Safrano::RelationManager.build_id(arg)
|
80
80
|
if @list.key?(rid)
|
81
81
|
@list[rid]
|
82
82
|
else
|
83
|
-
rel =
|
83
|
+
rel = Safrano::Relation.new(arg)
|
84
84
|
@list[rid] = rel
|
85
85
|
end
|
86
86
|
end
|
data/lib/odata/select.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'odata/error'
|
2
4
|
|
3
5
|
# all dataset selecting related classes in our OData module
|
4
6
|
# ie do eager loading
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# base class for selecting. We have to distinguish between
|
7
9
|
# fields of the current entity, and the navigation properties
|
8
10
|
# we can have one special case
|
@@ -12,12 +14,12 @@ module OData
|
|
12
14
|
class SelectBase
|
13
15
|
ALL = new # re-useable selecting-all handler
|
14
16
|
|
15
|
-
def self.factory(selectstr)
|
17
|
+
def self.factory(selectstr, model)
|
16
18
|
case selectstr&.strip
|
17
19
|
when nil, '', '*'
|
18
20
|
ALL
|
19
21
|
else
|
20
|
-
Select.new(selectstr)
|
22
|
+
Select.new(selectstr, model)
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
@@ -28,15 +30,25 @@ module OData
|
|
28
30
|
def ALL.all_props?
|
29
31
|
true
|
30
32
|
end
|
33
|
+
|
34
|
+
def parse_error?
|
35
|
+
Contract::OK
|
36
|
+
end
|
31
37
|
end
|
32
38
|
|
33
39
|
# single select
|
34
40
|
class Select < SelectBase
|
35
41
|
COMASPLIT = /\s*,\s*/.freeze
|
36
42
|
attr_reader :props
|
37
|
-
|
43
|
+
|
44
|
+
def initialize(selstr, model)
|
45
|
+
@model = model
|
38
46
|
@selectp = selstr.strip
|
39
47
|
@props = @selectp.split(COMASPLIT)
|
40
48
|
end
|
49
|
+
|
50
|
+
def parse_error?
|
51
|
+
(invalids = @model.find_invalid_props(props.to_set)) ? BadRequestSelectInvalidProps.new(@model, invalids) : Contract::OK
|
52
|
+
end
|
41
53
|
end
|
42
54
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'error'
|
4
|
+
|
5
|
+
# our main namespace
|
6
|
+
module Safrano
|
7
|
+
# represents a state transition when navigating/parsing the url path
|
8
|
+
# from left to right
|
9
|
+
class Transition < Regexp
|
10
|
+
attr_accessor :trans
|
11
|
+
attr_accessor :match_result
|
12
|
+
attr_accessor :rgx
|
13
|
+
attr_reader :remain_idx
|
14
|
+
|
15
|
+
EMPTYSTR = ''.freeze
|
16
|
+
SLASH = '/'.freeze
|
17
|
+
|
18
|
+
RESULT_BAD_REQ_ERR = [nil, :error, ::Safrano::BadRequestError].freeze
|
19
|
+
RESULT_NOT_FOUND_ERR = [nil, :error, ::Safrano::ErrorNotFound].freeze
|
20
|
+
RESULT_SERVER_TR_ERR = [nil, :error, ServerTransitionError].freeze
|
21
|
+
RESULT_END = [nil, :end].freeze
|
22
|
+
|
23
|
+
def initialize(arg, trans: nil, remain_idx: 2)
|
24
|
+
@rgx = if arg.respond_to? :each_char
|
25
|
+
Regexp.new(arg)
|
26
|
+
else
|
27
|
+
arg
|
28
|
+
end
|
29
|
+
@trans = trans
|
30
|
+
@remain_idx = remain_idx
|
31
|
+
end
|
32
|
+
|
33
|
+
def do_match(str)
|
34
|
+
@match_result = @rgx.match(str)
|
35
|
+
end
|
36
|
+
|
37
|
+
# remain_idx is the index of the last match-data. ususally its 2
|
38
|
+
# but can be overidden
|
39
|
+
def path_remain
|
40
|
+
@match_result[@remain_idx] if @match_result && @match_result[@remain_idx]
|
41
|
+
end
|
42
|
+
|
43
|
+
def path_done
|
44
|
+
if @match_result
|
45
|
+
@match_result[1] || EMPTYSTR
|
46
|
+
else
|
47
|
+
EMPTYSTR
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def do_transition(ctx)
|
52
|
+
ctx.method(@trans).call(@match_result)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
|
57
|
+
TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
|
58
|
+
trans: 'transition_metadata')
|
59
|
+
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
60
|
+
trans: 'transition_batch')
|
61
|
+
TransitionContentId = Transition.new('\A(\/\$(\d+))(.*)',
|
62
|
+
trans: 'transition_content_id',
|
63
|
+
remain_idx: 3)
|
64
|
+
TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
|
65
|
+
trans: 'transition_count')
|
66
|
+
TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
|
67
|
+
trans: 'transition_value')
|
68
|
+
TransitionLinks = Transition.new('(\A\/\$links)(.*)\z',
|
69
|
+
trans: 'transition_links')
|
70
|
+
attr_accessor :allowed_transitions
|
71
|
+
end
|
data/lib/odata/url_parameters.rb
CHANGED
@@ -1,23 +1,50 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'odata/error'
|
2
4
|
|
3
5
|
# url parameters processing . Mostly delegates to specialised classes
|
4
6
|
# (filter, order...) to convert into Sequel exprs.
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
class UrlParametersBase
|
7
9
|
attr_reader :expand
|
8
10
|
attr_reader :select
|
9
11
|
|
12
|
+
# url params validation methods.
|
13
|
+
# nil is the expected return for no errors
|
14
|
+
# an error class is returned in case of errors
|
15
|
+
# this way we can combine multiple validation calls with logical ||
|
10
16
|
def check_expand
|
11
|
-
|
17
|
+
@expand.parse_error?
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_select
|
21
|
+
@select.parse_error?
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(dataset, params = {})
|
25
|
+
@model = if dataset.respond_to? :model
|
26
|
+
dataset.model
|
27
|
+
else
|
28
|
+
dataset
|
29
|
+
end
|
30
|
+
@params = params
|
12
31
|
end
|
13
32
|
end
|
14
33
|
|
15
34
|
# url parameters for a single entity expand/select
|
16
35
|
class UrlParameters4Single < UrlParametersBase
|
17
|
-
def initialize(params)
|
18
|
-
|
19
|
-
@expand = ExpandBase.factory(@params['$expand'])
|
20
|
-
@select = SelectBase.factory(@params['$select'])
|
36
|
+
def initialize(dataset, params)
|
37
|
+
super
|
38
|
+
@expand = ExpandBase.factory(@params['$expand'], @model)
|
39
|
+
@select = SelectBase.factory(@params['$select'], @model)
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_all
|
43
|
+
return Contract::OK unless @params
|
44
|
+
|
45
|
+
check_expand.if_valid do
|
46
|
+
check_select
|
47
|
+
end
|
21
48
|
end
|
22
49
|
end
|
23
50
|
|
@@ -26,49 +53,98 @@ module OData
|
|
26
53
|
attr_reader :filt
|
27
54
|
attr_reader :ordby
|
28
55
|
|
29
|
-
def initialize(
|
56
|
+
def initialize(dataset, params = {})
|
57
|
+
super
|
30
58
|
# join helper is only needed for odering or filtering
|
31
|
-
@jh = model.join_by_paths_helper if params['$orderby'] || params['$filter']
|
59
|
+
@jh = @model.join_by_paths_helper if params['$orderby'] || params['$filter']
|
32
60
|
@params = params
|
33
61
|
@ordby = OrderBase.factory(@params['$orderby'], @jh)
|
34
62
|
@filt = FilterBase.factory(@params['$filter'])
|
35
|
-
@expand = ExpandBase.factory(@params['$expand'])
|
36
|
-
@select = SelectBase.factory(@params['$select'])
|
63
|
+
@expand = ExpandBase.factory(@params['$expand'], @model)
|
64
|
+
@select = SelectBase.factory(@params['$select'], @model)
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_top
|
68
|
+
return Contract::OK unless @params['$top']
|
69
|
+
|
70
|
+
itop = number_or_nil(@params['$top'])
|
71
|
+
(itop.nil? || itop.zero?) ? BadRequestError : Contract::OK
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_skip
|
75
|
+
return Contract::OK unless @params['$skip']
|
76
|
+
|
77
|
+
iskip = number_or_nil(@params['$skip'])
|
78
|
+
(iskip.nil? || iskip.negative?) ? BadRequestError : Contract::OK
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_inlinecount
|
82
|
+
return Contract::OK unless (icp = @params['$inlinecount'])
|
83
|
+
|
84
|
+
((icp == 'allpages') || (icp == 'none')) ? Contract::OK : BadRequestInlineCountParamError
|
37
85
|
end
|
38
86
|
|
39
87
|
def check_filter
|
40
|
-
|
88
|
+
(err = @filt.parse_error?) ? err : Contract::OK
|
41
89
|
end
|
42
90
|
|
43
|
-
def
|
91
|
+
def check_orderby
|
92
|
+
return Contract::OK if @ordby.empty?
|
44
93
|
return BadRequestOrderParseError if @ordby.parse_error?
|
94
|
+
|
95
|
+
Contract::OK
|
45
96
|
end
|
46
97
|
|
47
98
|
def apply_to_dataset(dtcx)
|
48
|
-
|
49
|
-
|
99
|
+
apply_expand_to_dataset(dtcx).if_valid do |dataset|
|
100
|
+
apply_filter_order_to_dataset(dataset)
|
101
|
+
end
|
50
102
|
end
|
51
103
|
|
52
104
|
def apply_expand_to_dataset(dtcx)
|
53
|
-
return dtcx if @expand.empty?
|
105
|
+
return Contract.valid(dtcx) if @expand.empty?
|
54
106
|
|
55
107
|
@expand.apply_to_dataset(dtcx)
|
56
108
|
end
|
57
109
|
|
58
110
|
# Warning, the @ordby and @filt objects are coupled by way of the join helper
|
59
111
|
def apply_filter_order_to_dataset(dtcx)
|
60
|
-
return dtcx if @filt.empty? && @ordby.empty?
|
112
|
+
return Contract.valid(dtcx) if @filt.empty? && @ordby.empty?
|
61
113
|
|
62
|
-
# filter object and join-helper need to be finalized after filter
|
63
|
-
|
114
|
+
# filter object and join-helper need to be finalized after filter
|
115
|
+
# has been parsed and checked
|
116
|
+
@filt.finalize(@jh).if_valid do
|
117
|
+
# start with the join
|
118
|
+
dtcx = @jh.dataset(dtcx)
|
64
119
|
|
65
|
-
|
66
|
-
|
120
|
+
@filt.apply_to_dataset(dtcx).map_result! do |dataset|
|
121
|
+
dtcx = dataset
|
122
|
+
dtcx = @ordby.apply_to_dataset(dtcx)
|
123
|
+
dtcx.select_all(@jh.start_model.table_name)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
67
127
|
|
68
|
-
|
69
|
-
|
128
|
+
###########################################################
|
129
|
+
def check_all
|
130
|
+
return Contract::OK unless @params
|
70
131
|
|
71
|
-
|
132
|
+
# lazy nested proc evaluation.
|
133
|
+
# if one check fails, it will be passed up the chain and the ones
|
134
|
+
# below will not be evaluated
|
135
|
+
check_top.if_valid do
|
136
|
+
check_skip.if_valid do
|
137
|
+
check_orderby.if_valid do
|
138
|
+
check_filter.if_valid do
|
139
|
+
check_expand.if_valid do
|
140
|
+
check_select.if_valid do
|
141
|
+
check_inlinecount
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
72
148
|
end
|
73
149
|
end
|
74
150
|
end
|
data/lib/odata/walker.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'rexml/document'
|
3
|
-
require 'safrano
|
5
|
+
require 'safrano'
|
4
6
|
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# handle navigation in the Datamodel tree of entities/attributes
|
7
9
|
# input is the url path. Url parameters ($filter etc...) are NOT handled here
|
8
10
|
# This uses a state transition algorithm
|
@@ -28,8 +30,12 @@ module OData
|
|
28
30
|
# are $links requested ?
|
29
31
|
attr_reader :do_links
|
30
32
|
|
33
|
+
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
34
|
+
EMPTYSTR = ''
|
35
|
+
SLASH = '/'
|
36
|
+
|
31
37
|
def initialize(service, path, content_id_refs = nil)
|
32
|
-
raise
|
38
|
+
raise NIL_SERVICE_FATAL unless service
|
33
39
|
|
34
40
|
path = URI.decode_www_form_component(path)
|
35
41
|
@context = service
|
@@ -40,10 +46,9 @@ module OData
|
|
40
46
|
@path_start = @path_remain = if service
|
41
47
|
unprefixed(service.xpath_prefix, path)
|
42
48
|
else # This is for batch function
|
43
|
-
|
44
49
|
path
|
45
50
|
end
|
46
|
-
@path_done =
|
51
|
+
@path_done = String.new
|
47
52
|
@status = :start
|
48
53
|
@end_context = nil
|
49
54
|
@do_count = nil
|
@@ -51,12 +56,12 @@ module OData
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def unprefixed(prefix, path)
|
54
|
-
if (prefix ==
|
59
|
+
if (prefix == EMPTYSTR) || (prefix == SLASH)
|
55
60
|
path
|
56
61
|
else
|
57
62
|
# path.sub!(/\A#{prefix}/, '')
|
58
63
|
# TODO check
|
59
|
-
path.sub(/\A#{prefix}/,
|
64
|
+
path.sub(/\A#{prefix}/, EMPTYSTR)
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
@@ -72,6 +77,7 @@ module OData
|
|
72
77
|
valid_tr = @context.allowed_transitions.select do |t|
|
73
78
|
t.do_match(@path_remain)
|
74
79
|
end
|
80
|
+
|
75
81
|
# this is a very fragile and obscure but required hack (wanted: a
|
76
82
|
# better one) to make attributes that are substrings of each other
|
77
83
|
# work well
|
@@ -93,13 +99,13 @@ module OData
|
|
93
99
|
@context = nil
|
94
100
|
@status = :error
|
95
101
|
# TODO: more appropriate error handling
|
96
|
-
@error =
|
102
|
+
@error = Safrano::ErrorNotFound
|
97
103
|
end
|
98
104
|
else
|
99
105
|
@context = nil
|
100
106
|
@status = :error
|
101
107
|
# TODO: more appropriate error handling
|
102
|
-
@error =
|
108
|
+
@error = Safrano::ErrorNotFound
|
103
109
|
end
|
104
110
|
end
|
105
111
|
|
@@ -149,7 +155,7 @@ module OData
|
|
149
155
|
else
|
150
156
|
@context = nil
|
151
157
|
@status = :error
|
152
|
-
@error =
|
158
|
+
@error = Safrano::ErrorNotFoundSegment.new(@path_remain)
|
153
159
|
end
|
154
160
|
end
|
155
161
|
# TODO: shouldnt we raise an error here if @status != :end ?
|
@@ -157,5 +163,9 @@ module OData
|
|
157
163
|
|
158
164
|
@end_context = @contexts.size >= 2 ? @contexts[-2] : @contexts[1]
|
159
165
|
end
|
166
|
+
|
167
|
+
def finalize
|
168
|
+
(@status == :end) ? Contract.valid(@end_context) : @error
|
169
|
+
end
|
160
170
|
end
|
161
171
|
end
|