safrano 0.4.1 → 0.4.6
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 +15 -13
- data/lib/odata/collection.rb +144 -535
- data/lib/odata/collection_filter.rb +47 -40
- data/lib/odata/collection_media.rb +155 -99
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +36 -34
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +183 -216
- data/lib/odata/error.rb +195 -31
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +44 -36
- data/lib/odata/filter/sequel.rb +136 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +113 -63
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +44 -61
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +54 -0
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +17 -37
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +29 -104
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +39 -43
- data/lib/safrano/rack_app.rb +68 -67
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +102 -51
- data/lib/safrano/response.rb +5 -3
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +274 -219
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +34 -11
@@ -1,19 +1,36 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
module OrderWithRuby
|
5
|
-
# this module requires the @fn attribute to exist where it is used
|
6
|
-
def fn=(fnam)
|
7
|
-
@fn = fnam
|
8
|
-
@fn_tab = fnam.split('/').map(&:to_sym)
|
9
|
-
end
|
10
|
-
end
|
3
|
+
require 'odata/error.rb'
|
11
4
|
|
12
5
|
# all ordering related classes in our OData module
|
13
|
-
module
|
6
|
+
module Safrano
|
14
7
|
# base class for ordering
|
15
|
-
class
|
8
|
+
class OrderBase
|
9
|
+
# re-useable empty ordering (idempotent)
|
10
|
+
EmptyOrder = new.freeze
|
11
|
+
|
12
|
+
# input : the OData order string
|
13
|
+
# returns a Order object that should have a apply_to(cx) method
|
14
|
+
def self.factory(orderstr, jh)
|
15
|
+
orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_error?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_to_dataset(dtcx)
|
27
|
+
dtcx
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Order < OrderBase
|
16
32
|
attr_reader :oarg
|
33
|
+
|
17
34
|
def initialize(ostr, jh)
|
18
35
|
ostr.strip!
|
19
36
|
@orderp = ostr
|
@@ -21,23 +38,14 @@ module OData
|
|
21
38
|
build_oarg if @orderp
|
22
39
|
end
|
23
40
|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
# input : the filter string
|
29
|
-
# returns a filter object that should have a apply_to(cx) method
|
30
|
-
def self.new_by_parse(orderstr, jh)
|
31
|
-
Order.new_full_match_complexpr(orderstr, jh)
|
32
|
-
end
|
33
|
-
|
34
|
-
# handle with Sequel
|
35
|
-
def self.new_full_match_complexpr(orderstr, jh)
|
36
|
-
ComplexOrder.new(orderstr, jh)
|
41
|
+
def empty?
|
42
|
+
false
|
37
43
|
end
|
38
44
|
|
39
45
|
def apply_to_dataset(dtcx)
|
40
|
-
|
46
|
+
# Warning, we need order_append, simply order(oarg) overwrites
|
47
|
+
# previous one !
|
48
|
+
dtcx.order_append(@oarg)
|
41
49
|
end
|
42
50
|
|
43
51
|
def build_oarg
|
@@ -60,26 +68,31 @@ module OData
|
|
60
68
|
end
|
61
69
|
|
62
70
|
# complex ordering logic
|
63
|
-
class
|
71
|
+
class MultiOrder < Order
|
64
72
|
def initialize(orderstr, jh)
|
65
73
|
super
|
66
74
|
@olist = []
|
67
75
|
@jh = jh
|
68
|
-
|
69
|
-
|
70
|
-
@olist = orderstr.split(',').map do |ostr|
|
71
|
-
oo = Order.new(ostr, @jh)
|
72
|
-
oo.oarg
|
73
|
-
end
|
76
|
+
@orderstr = orderstr.dup
|
77
|
+
@olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
|
74
78
|
end
|
75
79
|
|
76
80
|
def apply_to_dataset(dtcx)
|
77
|
-
@olist.each { |
|
78
|
-
# Warning, we need order_append, simply order(oarg) overwrites
|
79
|
-
# previous one !
|
80
|
-
dtcx = dtcx.order_append(oarg)
|
81
|
-
}
|
81
|
+
@olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
|
82
82
|
dtcx
|
83
83
|
end
|
84
|
+
|
85
|
+
def parse_error?
|
86
|
+
@orderstr.split(',').each do |pord|
|
87
|
+
pord.strip!
|
88
|
+
qualfn, dir = pord.split(/\s/)
|
89
|
+
qualfn.strip!
|
90
|
+
dir.strip! if dir
|
91
|
+
return true unless @jh.start_model.attrib_path_valid? qualfn
|
92
|
+
return true unless [nil, 'asc', 'desc'].include? dir
|
93
|
+
end
|
94
|
+
|
95
|
+
false
|
96
|
+
end
|
84
97
|
end
|
85
98
|
end
|
data/lib/odata/common_logger.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class ODataCommonLogger < CommonLogger
|
3
5
|
def call(env)
|
@@ -5,41 +7,41 @@ module Rack
|
|
5
7
|
super
|
6
8
|
end
|
7
9
|
|
8
|
-
# Handle https://github.com/rack/rack/pull/1526
|
10
|
+
# Handle https://github.com/rack/rack/pull/1526
|
9
11
|
# new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
|
10
|
-
|
11
|
-
MSG_FUNC = if
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
12
|
+
|
13
|
+
MSG_FUNC = if FORMAT.count('%') == 10
|
14
|
+
lambda { |env, length, status, began_at|
|
15
|
+
FORMAT % [
|
16
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
17
|
+
env['REMOTE_USER'] || '-',
|
18
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
19
|
+
env[REQUEST_METHOD],
|
20
|
+
env[SCRIPT_NAME] + env[PATH_INFO],
|
21
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
22
|
+
env[SERVER_PROTOCOL],
|
23
|
+
status.to_s[0..3],
|
24
|
+
length,
|
25
|
+
Utils.clock_time - began_at
|
26
|
+
]
|
27
|
+
}
|
28
|
+
elsif FORMAT.count('%') == 11
|
29
|
+
lambda { |env, length, status, began_at|
|
30
|
+
FORMAT % [
|
31
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
32
|
+
env['REMOTE_USER'] || '-',
|
33
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
34
|
+
env[REQUEST_METHOD],
|
35
|
+
env[SCRIPT_NAME],
|
36
|
+
env[PATH_INFO],
|
37
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
38
|
+
env[SERVER_PROTOCOL],
|
39
|
+
status.to_s[0..3],
|
40
|
+
length,
|
41
|
+
Utils.clock_time - began_at
|
42
|
+
]
|
43
|
+
}
|
44
|
+
end
|
43
45
|
|
44
46
|
def batch_log(env, status, header, began_at)
|
45
47
|
length = extract_content_length(header)
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
module FunctionImport
|
5
|
+
class ResultDefinition
|
6
|
+
D = 'd'
|
7
|
+
DJ_OPEN = '{"d":'
|
8
|
+
DJ_CLOSE = '}'
|
9
|
+
METAK = '__metadata'
|
10
|
+
TYPEK = 'type'
|
11
|
+
VALUEK = 'value'
|
12
|
+
RESULTSK = 'results'
|
13
|
+
COLLECTION = 'Collection'
|
14
|
+
|
15
|
+
def initialize(klassmod)
|
16
|
+
@klassmod = klassmod
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_odata_json(result, _req)
|
20
|
+
"#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def type_metadata
|
24
|
+
@klassmod.type_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class ResultAsComplexType < ResultDefinition
|
28
|
+
end
|
29
|
+
class ResultAsComplexTypeColl < ResultDefinition
|
30
|
+
def type_metadata
|
31
|
+
"Collection(#{@klassmod.type_name})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_odata_json(coll, _req)
|
35
|
+
"#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
class ResultAsEntity < ResultDefinition
|
39
|
+
def to_odata_json(result_entity, req)
|
40
|
+
result_entity.instance_exec do
|
41
|
+
copy_request_infos(req)
|
42
|
+
to_odata_json(request: req)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
class ResultAsEntityColl < ResultDefinition
|
47
|
+
def type_metadata
|
48
|
+
"Collection(#{@klassmod.type_name})"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_odata_json(result_dataset, req)
|
52
|
+
coll = Safrano::OData::Collection.new(@klassmod)
|
53
|
+
coll.instance_exec do
|
54
|
+
@params = req.params
|
55
|
+
initialize_dataset(result_dataset)
|
56
|
+
end
|
57
|
+
coll.to_odata_json(request: req)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
class ResultAsPrimitiveType < ResultDefinition
|
61
|
+
def to_odata_json(result, _req)
|
62
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
63
|
+
VALUEK => @klassmod.odata_value(result) } }.to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
class ResultAsPrimitiveTypeColl < ResultDefinition
|
67
|
+
def type_metadata
|
68
|
+
"Collection(#{@klassmod.type_name})"
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_odata_json(result, _req)
|
72
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
+
RESULTSK => @klassmod.odata_collection(result) } }.to_json
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# a generic Struct like ruby's standard Struct, but implemented with a
|
79
|
+
# @values Hash, similar to Sequel models and
|
80
|
+
# with added OData functionality
|
81
|
+
class ComplexType
|
82
|
+
attr_reader :values
|
83
|
+
|
84
|
+
@namespace = nil
|
85
|
+
def self.namespace
|
86
|
+
@namespace
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.props
|
90
|
+
@props
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.type_name
|
94
|
+
"#{@namespace}.#{self.to_s}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@values = {}
|
99
|
+
end
|
100
|
+
METAK = '__metadata'
|
101
|
+
TYPEK = 'type'
|
102
|
+
|
103
|
+
def odata_h
|
104
|
+
ret = { METAK => { TYPEK => self.class.type_name } }
|
105
|
+
@values.each { |k, v|
|
106
|
+
ret[k] = if v.respond_to? :odata_h
|
107
|
+
v.odata_h
|
108
|
+
else
|
109
|
+
v
|
110
|
+
end
|
111
|
+
}
|
112
|
+
ret
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.return_as_collection_descriptor
|
116
|
+
FunctionImport::ResultAsComplexTypeColl.new(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.return_as_instance_descriptor
|
120
|
+
FunctionImport::ResultAsComplexType.new(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
# add metadata xml to the passed REXML schema object
|
124
|
+
def self.add_metadata_rexml(schema)
|
125
|
+
ctty = schema.add_element('ComplexType', 'Name' => to_s)
|
126
|
+
|
127
|
+
# with their properties
|
128
|
+
@props.each do |prop, rbtype|
|
129
|
+
attrs = { 'Name' => prop.to_s,
|
130
|
+
'Type' => rbtype.type_name }
|
131
|
+
ctty.add_element('Property', attrs)
|
132
|
+
end
|
133
|
+
ctty
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def Safrano.ComplexType(**props)
|
138
|
+
Class.new(Safrano::ComplexType) do
|
139
|
+
@props = props
|
140
|
+
props.each { |a, klassmod|
|
141
|
+
asym = a.to_sym
|
142
|
+
define_method(asym) do @values[asym] end
|
143
|
+
define_method("#{a}=") do |val| @values[asym] = val end
|
144
|
+
}
|
145
|
+
define_method :initialize do |*p, **kwvals|
|
146
|
+
super()
|
147
|
+
p.zip(props.keys).each { |val, a| @values[a] = val } if p
|
148
|
+
kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Safrano
|
6
|
+
# Type mapping DB --> Edm
|
7
|
+
# TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
|
8
|
+
# "STRING" => "Edm.String"}
|
9
|
+
# Todo: complete mapping... this is just for the most common ones
|
10
|
+
|
11
|
+
# TODO: use Sequel GENERIC_TYPES: -->
|
12
|
+
# Constants
|
13
|
+
# GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
|
14
|
+
# Time File TrueClass FalseClass'.freeze
|
15
|
+
# Classes specifying generic types that Sequel will convert to
|
16
|
+
# database-specific types.
|
17
|
+
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
18
|
+
|
19
|
+
# used in $metadata
|
20
|
+
# cf. Sequel Database column_schema_default_to_ruby_value
|
21
|
+
# schema_column_type
|
22
|
+
# https://www.odata.org/documentation/odata-version-2-0/overview/
|
23
|
+
def self.default_edm_type(ruby_type:)
|
24
|
+
case ruby_type
|
25
|
+
when :integer
|
26
|
+
'Edm.Int32'
|
27
|
+
when :string
|
28
|
+
'Edm.String'
|
29
|
+
when :date, :datetime,
|
30
|
+
'Edm.DateTime'
|
31
|
+
when :time
|
32
|
+
'Edm.Time'
|
33
|
+
when :boolean
|
34
|
+
'Edm.Boolean'
|
35
|
+
when :float
|
36
|
+
'Edm.Double'
|
37
|
+
when :decimal
|
38
|
+
'Edm.Decimal'
|
39
|
+
when :blob
|
40
|
+
'Edm.Binary'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# use Edm twice so that we can do include Safrano::Edm and then
|
45
|
+
# have Edm::Int32 etc... availabe
|
46
|
+
# and we can have Edm::String different from ::String
|
47
|
+
module Edm
|
48
|
+
module Edm
|
49
|
+
module OutputClassMethods
|
50
|
+
def type_name
|
51
|
+
"Edm.#{name.split('::').last}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def odata_collection(array)
|
55
|
+
array
|
56
|
+
end
|
57
|
+
|
58
|
+
def odata_value(instance)
|
59
|
+
instance
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Null < NilClass
|
64
|
+
extend OutputClassMethods
|
65
|
+
# nil --> null convertion is done by to_json
|
66
|
+
def self.odata_value(instance)
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.convert_from_urlparam(v)
|
71
|
+
return Contract::NOK unless (v == 'null')
|
72
|
+
|
73
|
+
Contract.valid(nil)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Binary is a String with the BINARY encoding
|
78
|
+
class Binary < String
|
79
|
+
extend OutputClassMethods
|
80
|
+
|
81
|
+
def self.convert_from_urlparam(v)
|
82
|
+
Contract.valid(v.dup.force_encoding('BINARY'))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# an object alwys evaluates to
|
87
|
+
# true ([true, anything not false & not nil objs])
|
88
|
+
# or false([nil, false])
|
89
|
+
class Boolean < Object
|
90
|
+
extend OutputClassMethods
|
91
|
+
def Boolean.odata_value(instance)
|
92
|
+
instance ? true : false
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.odata_collection(array)
|
96
|
+
array.map { |v| odata_value(v) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.convert_from_urlparam(v)
|
100
|
+
return Contract::NOK unless ['true', 'false'].include?(v)
|
101
|
+
|
102
|
+
Contract.valid(v == 'true')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Bytes are usualy represented as Intger in ruby,
|
107
|
+
# eg.String.bytes --> Array of ints
|
108
|
+
class Byte < Integer
|
109
|
+
extend OutputClassMethods
|
110
|
+
|
111
|
+
def self.convert_from_urlparam(v)
|
112
|
+
return Contract::NOK unless ((bytev = v.to_i) < 256)
|
113
|
+
|
114
|
+
Contract.valid(bytev)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class DateTime < ::DateTime
|
119
|
+
extend OutputClassMethods
|
120
|
+
def DateTime.odata_value(instance)
|
121
|
+
instance.to_datetime
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.odata_collection(array)
|
125
|
+
array.map { |v| odata_value(v) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.convert_from_urlparam(v)
|
129
|
+
begin
|
130
|
+
Contract.valid(DateTime.parse(v))
|
131
|
+
rescue
|
132
|
+
return convertion_error(v)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class String < ::String
|
138
|
+
extend OutputClassMethods
|
139
|
+
|
140
|
+
def self.convert_from_urlparam(v)
|
141
|
+
Contract.valid(v)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Int32 < Integer
|
146
|
+
extend OutputClassMethods
|
147
|
+
|
148
|
+
def self.convert_from_urlparam(v)
|
149
|
+
return Contract::NOK unless (ret = number_or_nil(v))
|
150
|
+
|
151
|
+
Contract.valid(ret)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class Int64 < Integer
|
156
|
+
extend OutputClassMethods
|
157
|
+
|
158
|
+
def self.convert_from_urlparam(v)
|
159
|
+
return Contract::NOK unless (ret = number_or_nil(v))
|
160
|
+
|
161
|
+
Contract.valid(ret)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Double < Float
|
166
|
+
extend OutputClassMethods
|
167
|
+
|
168
|
+
def self.convert_from_urlparam(v)
|
169
|
+
begin
|
170
|
+
Contract.valid(v.to_f)
|
171
|
+
rescue
|
172
|
+
return Contract::NOK
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# include Safrano
|
181
|
+
|
182
|
+
# x = Edm::String.new('xxx')
|
183
|
+
|
184
|
+
# pp x
|