saxon-rb 0.4.0-java
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 +7 -0
- data/.circleci/config.yml +62 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +20 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/net/sf/saxon/Saxon-HE/9.9.1-5/Saxon-HE-9.9.1-5.jar +0 -0
- data/lib/saxon.rb +6 -0
- data/lib/saxon/axis_iterator.rb +31 -0
- data/lib/saxon/configuration.rb +116 -0
- data/lib/saxon/document_builder.rb +28 -0
- data/lib/saxon/item_type.rb +290 -0
- data/lib/saxon/item_type/lexical_string_conversion.rb +383 -0
- data/lib/saxon/item_type/value_to_ruby.rb +78 -0
- data/lib/saxon/jaxp.rb +8 -0
- data/lib/saxon/loader.rb +93 -0
- data/lib/saxon/occurrence_indicator.rb +33 -0
- data/lib/saxon/parse_options.rb +127 -0
- data/lib/saxon/processor.rb +102 -0
- data/lib/saxon/qname.rb +153 -0
- data/lib/saxon/s9api.rb +34 -0
- data/lib/saxon/serializer.rb +143 -0
- data/lib/saxon/source.rb +187 -0
- data/lib/saxon/version.rb +3 -0
- data/lib/saxon/xdm.rb +35 -0
- data/lib/saxon/xdm/array.rb +77 -0
- data/lib/saxon/xdm/atomic_value.rb +173 -0
- data/lib/saxon/xdm/empty_sequence.rb +37 -0
- data/lib/saxon/xdm/external_object.rb +21 -0
- data/lib/saxon/xdm/function_item.rb +21 -0
- data/lib/saxon/xdm/item.rb +32 -0
- data/lib/saxon/xdm/map.rb +77 -0
- data/lib/saxon/xdm/node.rb +71 -0
- data/lib/saxon/xdm/sequence_like.rb +30 -0
- data/lib/saxon/xdm/value.rb +145 -0
- data/lib/saxon/xpath.rb +8 -0
- data/lib/saxon/xpath/compiler.rb +69 -0
- data/lib/saxon/xpath/executable.rb +58 -0
- data/lib/saxon/xpath/static_context.rb +161 -0
- data/lib/saxon/xpath/variable_declaration.rb +68 -0
- data/lib/saxon/xslt.rb +8 -0
- data/lib/saxon/xslt/compiler.rb +70 -0
- data/lib/saxon/xslt/evaluation_context.rb +165 -0
- data/lib/saxon/xslt/executable.rb +156 -0
- data/lib/saxon_jars.rb +10 -0
- data/saxon-rb.gemspec +39 -0
- data/saxon.gemspec +30 -0
- metadata +240 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'saxon/xdm'
|
2
|
+
|
3
|
+
module Saxon
|
4
|
+
# Builds XDM objects from XML sources, for use in XSLT or for query and
|
5
|
+
# access
|
6
|
+
class DocumentBuilder
|
7
|
+
# @api private
|
8
|
+
# @param [net.sf.saxon.s9api.DocumentBuilder] s9_document_builder The
|
9
|
+
# Saxon DocumentBuilder instance to wrap
|
10
|
+
def initialize(s9_document_builder)
|
11
|
+
@s9_document_builder = s9_document_builder
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Saxon::Source] source The Saxon::Source containing the source
|
15
|
+
# IO/string
|
16
|
+
# @return [Saxon::XDM::Node] The Saxon::XDM::Node representing the root of the
|
17
|
+
# document tree
|
18
|
+
def build(source)
|
19
|
+
XDM::Node.new(@s9_document_builder.build(source.to_java))
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [net.sf.saxon.s9api.DocumentBuilder] The underlying Java Saxon
|
23
|
+
# DocumentBuilder instance
|
24
|
+
def to_java
|
25
|
+
@s9_document_builder
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require_relative 's9api'
|
2
|
+
require_relative 'qname'
|
3
|
+
require_relative 'item_type/lexical_string_conversion'
|
4
|
+
require_relative 'item_type/value_to_ruby'
|
5
|
+
|
6
|
+
module Saxon
|
7
|
+
# Represent XDM types abstractly
|
8
|
+
class ItemType
|
9
|
+
# Error raised when a Ruby class has no equivalent XDM type to be converted
|
10
|
+
# into
|
11
|
+
class UnmappedRubyTypeError < StandardError
|
12
|
+
def initialize(class_name)
|
13
|
+
@class_name = class_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"Ruby class <#{@class_name}> has no XDM type equivalent"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Error raise when an attempt to reify an <tt>xs:*</tt> type string is
|
22
|
+
# made, but the type string doesn't match any of the built-in <tt>xs:*</tt>
|
23
|
+
# types
|
24
|
+
class UnmappedXSDTypeNameError < StandardError
|
25
|
+
def initialize(type_str)
|
26
|
+
@type_str = type_str
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"'#{@type_str}' is not recognised as an XSD built-in type"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Factory
|
35
|
+
DEFAULT_SEMAPHORE = Mutex.new
|
36
|
+
|
37
|
+
attr_reader :processor
|
38
|
+
|
39
|
+
def initialize(processor)
|
40
|
+
@processor = processor
|
41
|
+
end
|
42
|
+
|
43
|
+
def s9_factory
|
44
|
+
return @s9_factory if instance_variable_defined?(:@s9_factory)
|
45
|
+
DEFAULT_SEMAPHORE.synchronize do
|
46
|
+
@s9_factory = S9API::ItemTypeFactory.new(processor.to_java)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
TYPE_CACHE_MUTEX = Mutex.new
|
52
|
+
# A mapping of Ruby types to XDM type constants
|
53
|
+
TYPE_MAPPING = {
|
54
|
+
'String' => :STRING,
|
55
|
+
'Array' => :ANY_ARRAY,
|
56
|
+
'Hash' => :ANY_MAP,
|
57
|
+
'TrueClass' => :BOOLEAN,
|
58
|
+
'FalseClass' => :BOOLEAN,
|
59
|
+
'Date' => :DATE,
|
60
|
+
'DateTime' => :DATE_TIME,
|
61
|
+
'Time' => :DATE_TIME,
|
62
|
+
'BigDecimal' => :DECIMAL,
|
63
|
+
'Integer' => :INTEGER,
|
64
|
+
'Float' => :FLOAT,
|
65
|
+
'Numeric' => :NUMERIC
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
# A mapping of QNames to XDM type constants
|
69
|
+
QNAME_MAPPING = Hash[{
|
70
|
+
'anyAtomicType' => :ANY_ATOMIC_VALUE,
|
71
|
+
'anyURI' => :ANY_URI,
|
72
|
+
'base64Binary' => :BASE64_BINARY,
|
73
|
+
'boolean' => :BOOLEAN,
|
74
|
+
'byte' => :BYTE,
|
75
|
+
'date' => :DATE,
|
76
|
+
'dateTime' => :DATE_TIME,
|
77
|
+
'dateTimeStamp' => :DATE_TIME_STAMP,
|
78
|
+
'dayTimeDuration' => :DAY_TIME_DURATION,
|
79
|
+
'decimal' => :DECIMAL,
|
80
|
+
'double' => :DOUBLE,
|
81
|
+
'duration' => :DURATION,
|
82
|
+
'ENTITY' => :ENTITY,
|
83
|
+
'float' => :FLOAT,
|
84
|
+
'gDay' => :G_DAY,
|
85
|
+
'gMonth' => :G_MONTH,
|
86
|
+
'gMonthDay' => :G_MONTH_DAY,
|
87
|
+
'gYear' => :G_YEAR,
|
88
|
+
'gYearMonth' => :G_YEAR_MONTH,
|
89
|
+
'hexBinary' => :HEX_BINARY,
|
90
|
+
'ID' => :ID,
|
91
|
+
'IDREF' => :IDREF,
|
92
|
+
'int' => :INT,
|
93
|
+
'integer' => :INTEGER,
|
94
|
+
'language' => :LANGUAGE,
|
95
|
+
'long' => :LONG,
|
96
|
+
'Name' => :NAME,
|
97
|
+
'NCName' => :NCNAME,
|
98
|
+
'negativeInteger' => :NEGATIVE_INTEGER,
|
99
|
+
'NMTOKEN' => :NMTOKEN,
|
100
|
+
'nonNegativeInteger' => :NON_NEGATIVE_INTEGER,
|
101
|
+
'nonPositiveInteger' => :NON_POSITIVE_INTEGER,
|
102
|
+
'normalizedString' => :NORMALIZED_STRING,
|
103
|
+
'NOTATION' => :NOTATION,
|
104
|
+
'numeric' => :NUMERIC,
|
105
|
+
'positiveInteger' => :POSITIVE_INTEGER,
|
106
|
+
'QName' => :QNAME,
|
107
|
+
'short' => :SHORT,
|
108
|
+
'string' => :STRING,
|
109
|
+
'time' => :TIME,
|
110
|
+
'token' => :TOKEN,
|
111
|
+
'unsignedByte' => :UNSIGNED_BYTE,
|
112
|
+
'unsignedInt' => :UNSIGNED_INT,
|
113
|
+
'unsignedLong' => :UNSIGNED_LONG,
|
114
|
+
'unsignedShort' => :UNSIGNED_SHORT,
|
115
|
+
'untypedAtomic' => :UNTYPED_ATOMIC,
|
116
|
+
'yearMonthDuration' => :YEAR_MONTH_DURATION
|
117
|
+
}.map { |local_name, constant|
|
118
|
+
qname = Saxon::QName.create({
|
119
|
+
prefix: 'xs', uri: 'http://www.w3.org/2001/XMLSchema',
|
120
|
+
local_name: local_name
|
121
|
+
})
|
122
|
+
[qname, constant]
|
123
|
+
}].freeze
|
124
|
+
|
125
|
+
# A mapping of type names/QNames to XDM type constants
|
126
|
+
STR_MAPPING = {
|
127
|
+
'array(*)' => :ANY_ARRAY,
|
128
|
+
'item()' => :ANY_ITEM,
|
129
|
+
'map(*)' => :ANY_MAP,
|
130
|
+
'node()' => :ANY_NODE
|
131
|
+
}.merge(
|
132
|
+
Hash[QNAME_MAPPING.map { |qname, v| [qname.to_s, v] }]
|
133
|
+
).freeze
|
134
|
+
|
135
|
+
ATOMIC_VALUE_LEXICAL_STRING_CONVERTORS = Hash[
|
136
|
+
LexicalStringConversion::Convertors.constants.map { |const|
|
137
|
+
[S9API::ItemType.const_get(const), LexicalStringConversion::Convertors.const_get(const)]
|
138
|
+
}
|
139
|
+
].freeze
|
140
|
+
|
141
|
+
ATOMIC_VALUE_TO_RUBY_CONVERTORS = Hash[
|
142
|
+
ValueToRuby::Convertors.constants.map { |const|
|
143
|
+
[S9API::ItemType.const_get(const), ValueToRuby::Convertors.const_get(const)]
|
144
|
+
}
|
145
|
+
].freeze
|
146
|
+
|
147
|
+
class << self
|
148
|
+
# Get an appropriate {ItemType} for a Ruby type or given a type name as a
|
149
|
+
# string
|
150
|
+
#
|
151
|
+
# @return [Saxon::ItemType]
|
152
|
+
# @overload get_type(ruby_class)
|
153
|
+
# Get an appropriate {ItemType} for object of a given Ruby class
|
154
|
+
# @param ruby_class [Class] The Ruby class to get a type for
|
155
|
+
# @overload get_type(type_name)
|
156
|
+
# Get the {ItemType} for the name
|
157
|
+
# @param type_name [String] name of the built-in {ItemType} to fetch
|
158
|
+
# @overload get_type(item_type)
|
159
|
+
# Given an instance of {ItemType}, simply return the instance
|
160
|
+
# @param item_type [Saxon::ItemType] an existing ItemType instance
|
161
|
+
def get_type(arg)
|
162
|
+
case arg
|
163
|
+
when Saxon::ItemType
|
164
|
+
arg
|
165
|
+
else
|
166
|
+
fetch_type_instance(get_s9_type(arg))
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def fetch_type_instance(s9_type)
|
173
|
+
TYPE_CACHE_MUTEX.synchronize do
|
174
|
+
@type_instance_cache = {} if !instance_variable_defined?(:@type_instance_cache)
|
175
|
+
if type_instance = @type_instance_cache[s9_type]
|
176
|
+
type_instance
|
177
|
+
else
|
178
|
+
@type_instance_cache[s9_type] = new(s9_type)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_s9_type(arg)
|
184
|
+
case arg
|
185
|
+
when Saxon::QName
|
186
|
+
get_s9_qname_mapped_type(arg)
|
187
|
+
when Class
|
188
|
+
get_s9_class_mapped_type(arg)
|
189
|
+
when String
|
190
|
+
get_s9_str_mapped_type(arg)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_s9_qname_mapped_type(qname)
|
195
|
+
if mapped_type = QNAME_MAPPING.fetch(qname, false)
|
196
|
+
S9API::ItemType.const_get(mapped_type)
|
197
|
+
else
|
198
|
+
raise UnmappedXSDTypeNameError, qname.to_s
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def get_s9_class_mapped_type(klass)
|
203
|
+
class_name = klass.name
|
204
|
+
if mapped_type = TYPE_MAPPING.fetch(class_name, false)
|
205
|
+
S9API::ItemType.const_get(mapped_type)
|
206
|
+
else
|
207
|
+
raise UnmappedRubyTypeError, class_name
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_s9_str_mapped_type(type_str)
|
212
|
+
if mapped_type = STR_MAPPING.fetch(type_str, false)
|
213
|
+
# ANY_ITEM is a method, not a constant, for reasons not entirely
|
214
|
+
# clear to me
|
215
|
+
return S9API::ItemType.ANY_ITEM if mapped_type == :ANY_ITEM
|
216
|
+
S9API::ItemType.const_get(mapped_type)
|
217
|
+
else
|
218
|
+
raise UnmappedXSDTypeNameError, type_str
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
attr_reader :s9_item_type
|
224
|
+
private :s9_item_type
|
225
|
+
|
226
|
+
# @api private
|
227
|
+
def initialize(s9_item_type)
|
228
|
+
@s9_item_type = s9_item_type
|
229
|
+
end
|
230
|
+
|
231
|
+
# Return the {QName} which represents this type
|
232
|
+
#
|
233
|
+
# @return [Saxon::QName] the {QName} of the type
|
234
|
+
def type_name
|
235
|
+
@type_name ||= Saxon::QName.new(s9_item_type.getTypeName)
|
236
|
+
end
|
237
|
+
|
238
|
+
# @return [Saxon::S9API::ItemType] The underlying Saxon Java ItemType object
|
239
|
+
def to_java
|
240
|
+
s9_item_type
|
241
|
+
end
|
242
|
+
|
243
|
+
# compares two {ItemType}s using the underlying Saxon and XDM comparision rules
|
244
|
+
# @param other [Saxon::ItemType]
|
245
|
+
# @return [Boolean]
|
246
|
+
def ==(other)
|
247
|
+
return false unless other.is_a?(ItemType)
|
248
|
+
s9_item_type.equals(other.to_java)
|
249
|
+
end
|
250
|
+
|
251
|
+
alias_method :eql?, :==
|
252
|
+
|
253
|
+
def hash
|
254
|
+
@hash ||= s9_item_type.hashCode
|
255
|
+
end
|
256
|
+
|
257
|
+
# Generate the appropriate lexical string representation of the value
|
258
|
+
# given the ItemType's schema definition.
|
259
|
+
#
|
260
|
+
# Types with no explcit formatter defined just get to_s called on them...
|
261
|
+
#
|
262
|
+
# @param value [Object] The Ruby value to generate the lexical string
|
263
|
+
# representation of
|
264
|
+
# @return [String] The XML Schema-defined lexical string representation of
|
265
|
+
# the value
|
266
|
+
def lexical_string(value)
|
267
|
+
lexical_string_convertor.call(value, self)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Convert an XDM Atomic Value to an instance of an appropriate Ruby class, or return the lexical string.
|
271
|
+
#
|
272
|
+
# It's assumed that the XDM::AtomicValue is of this type, otherwise an error is raised.
|
273
|
+
# @param xdm_atomic_value [Saxon::XDM::AtomicValue] The XDM atomic value to be converted.
|
274
|
+
def ruby_value(xdm_atomic_value)
|
275
|
+
value_to_ruby_convertor.call(xdm_atomic_value)
|
276
|
+
end
|
277
|
+
|
278
|
+
private
|
279
|
+
|
280
|
+
def lexical_string_convertor
|
281
|
+
@lexical_string_convertor ||= ATOMIC_VALUE_LEXICAL_STRING_CONVERTORS.fetch(s9_item_type, ->(value, item) { value.to_s })
|
282
|
+
end
|
283
|
+
|
284
|
+
def value_to_ruby_convertor
|
285
|
+
@value_to_ruby_convertor ||= ATOMIC_VALUE_TO_RUBY_CONVERTORS.fetch(s9_item_type, ->(xdm_atomic_value) {
|
286
|
+
xdm_atomic_value.to_s
|
287
|
+
})
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,383 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Saxon
|
4
|
+
class ItemType
|
5
|
+
# A collection of lamba-like objects for converting Ruby values into
|
6
|
+
# lexical strings for specific XSD datatypes
|
7
|
+
module LexicalStringConversion
|
8
|
+
def self.validate(value, item_type, pattern)
|
9
|
+
str = value.to_s
|
10
|
+
raise Errors::BadRubyValue.new(value, item_type) unless str.match?(pattern)
|
11
|
+
str
|
12
|
+
end
|
13
|
+
|
14
|
+
class IntegerConversion
|
15
|
+
attr_reader :min, :max
|
16
|
+
|
17
|
+
def initialize(min, max)
|
18
|
+
@min, @max = min, max
|
19
|
+
end
|
20
|
+
|
21
|
+
def in_bounds?(integer_value)
|
22
|
+
gte_min?(integer_value) && lte_max?(integer_value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def gte_min?(integer_value)
|
26
|
+
return true if min.nil?
|
27
|
+
integer_value >= min
|
28
|
+
end
|
29
|
+
|
30
|
+
def lte_max?(integer_value)
|
31
|
+
return true if max.nil?
|
32
|
+
integer_value <= max
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(value, item_type)
|
36
|
+
integer_value = case value
|
37
|
+
when ::Numeric
|
38
|
+
value.to_i
|
39
|
+
else
|
40
|
+
Integer(LexicalStringConversion.validate(value, item_type, Patterns::INTEGER), 10)
|
41
|
+
end
|
42
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type) unless in_bounds?(integer_value)
|
43
|
+
integer_value.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class FloatConversion
|
48
|
+
def initialize(size = :double)
|
49
|
+
@double = size == :double
|
50
|
+
end
|
51
|
+
|
52
|
+
def double?
|
53
|
+
@double
|
54
|
+
end
|
55
|
+
|
56
|
+
def float_value(float_value)
|
57
|
+
return float_value if double?
|
58
|
+
convert_to_single_precision(float_value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def convert_to_single_precision(float_value)
|
62
|
+
[float_value].pack('f').unpack('f').first
|
63
|
+
end
|
64
|
+
|
65
|
+
def call(value, item_type)
|
66
|
+
case value
|
67
|
+
when ::Float::INFINITY
|
68
|
+
'INF'
|
69
|
+
when -::Float::INFINITY
|
70
|
+
'-INF'
|
71
|
+
when Numeric
|
72
|
+
float_value(value).to_s
|
73
|
+
else
|
74
|
+
LexicalStringConversion.validate(value, item_type, Patterns::FLOAT)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class GDateConversion
|
80
|
+
attr_reader :bounds, :integer_formatter, :validation_pattern
|
81
|
+
|
82
|
+
def initialize(args = {})
|
83
|
+
@bounds = args.fetch(:bounds)
|
84
|
+
@validation_pattern = args.fetch(:validation_pattern)
|
85
|
+
@integer_formatter = args.fetch(:integer_formatter)
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_value_from_validated_format(formatted_value)
|
89
|
+
Integer(formatted_value.gsub(validation_pattern, '\1'), 10)
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_value_bounds!(value, item_type)
|
93
|
+
bounds_method = bounds.respond_to?(:include?) ? :include? : :call
|
94
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type) unless bounds.send(bounds_method, value)
|
95
|
+
end
|
96
|
+
|
97
|
+
def extract_and_check_value_bounds!(formatted_value, item_type)
|
98
|
+
check_value_bounds!(extract_value_from_validated_format(formatted_value), item_type)
|
99
|
+
end
|
100
|
+
|
101
|
+
def call(value, item_type)
|
102
|
+
case value
|
103
|
+
when Integer
|
104
|
+
check_value_bounds!(value, item_type)
|
105
|
+
sprintf(integer_formatter.call(value), value)
|
106
|
+
else
|
107
|
+
formatted_value = LexicalStringConversion.validate(value, item_type, validation_pattern)
|
108
|
+
extract_and_check_value_bounds!(formatted_value, item_type)
|
109
|
+
formatted_value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module PatternFragments
|
115
|
+
TIME_DURATION = /(?:T
|
116
|
+
(?:
|
117
|
+
# The time part of the format allows T0H1M1S, T1H1M, T1M1S, T1H1S, T1H, T1M, T1S
|
118
|
+
[0-9]+[HM]|
|
119
|
+
[0-9]+(?:\.[0-9]+)?S|
|
120
|
+
[0-9]+H[0-9]+M|
|
121
|
+
[0-9]+H[0-9]+(?:\.[0-9]+)?S|
|
122
|
+
[0-9]+M[0-9]+(?:\.[0-9]+)?S|
|
123
|
+
[0-9]+H[0-9]+M[0-9]+(?:\.[0-9]+)?S
|
124
|
+
)
|
125
|
+
)?/x
|
126
|
+
DATE = /-?[0-9]{4}-[0-9]{2}-[0-9]{2}/
|
127
|
+
TIME = /[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?/
|
128
|
+
TIME_ZONE = /(?:[\-+][0-9]{2}:[0-9]{2}|Z)?/
|
129
|
+
NCNAME_START_CHAR = '[A-Z]|_|[a-z]|[\u{C0}-\u{D6}]|[\u{D8}-\u{F6}]|[\u{F8}-\u{2FF}]|[\u{370}-\u{37D}]|[\u{37F}-\u{1FFF}]|[\u{200C}-\u{200D}]|[\u{2070}-\u{218F}]|[\u{2C00}-\u{2FEF}]|[\u{3001}-\u{D7FF}]|[\u{F900}-\u{FDCF}]|[\u{FDF0}-\u{FFFD}]|[\u{10000}-\u{EFFFF}]'
|
130
|
+
NAME_START_CHAR = ":|" + NCNAME_START_CHAR
|
131
|
+
NCNAME_CHAR = NCNAME_START_CHAR + '|-|\.|[0-9]|\u{B7}|[\u{0300}-\u{036F}]|[\u{203F}-\u{2040}]'
|
132
|
+
NAME_CHAR = ":|" + NCNAME_CHAR
|
133
|
+
end
|
134
|
+
|
135
|
+
module Patterns
|
136
|
+
def self.build(*patterns)
|
137
|
+
Regexp.new((['\A'] + patterns.map(&:to_s) + ['\z']).join(''))
|
138
|
+
end
|
139
|
+
DATE = build(PatternFragments::DATE, PatternFragments::TIME_ZONE)
|
140
|
+
DATE_TIME = build(PatternFragments::DATE, 'T', PatternFragments::TIME, PatternFragments::TIME_ZONE)
|
141
|
+
TIME = build(PatternFragments::TIME, PatternFragments::TIME_ZONE)
|
142
|
+
DURATION = build(/-?P(?!\z)(?:[0-9]+Y)?(?:[0-9]+M)?(?:[0-9]+D)?/, PatternFragments::TIME_DURATION)
|
143
|
+
DAY_TIME_DURATION = build(/-?P(?!\z)(?:[0-9]+D)?/, PatternFragments::TIME_DURATION)
|
144
|
+
YEAR_MONTH_DURATION = /\A-?P(?!\z)(?:[0-9]+Y)?(?:[0-9]+M)?\z/
|
145
|
+
G_DAY = build(/---([0-9]{2})/, PatternFragments::TIME_ZONE)
|
146
|
+
G_MONTH = build(/--([0-9]{2})/, PatternFragments::TIME_ZONE)
|
147
|
+
G_YEAR = build(/(-?[0-9]{4,})/, PatternFragments::TIME_ZONE)
|
148
|
+
G_YEAR_MONTH = build(/-?([0-9]{4,})-([0-9]{2})/, PatternFragments::TIME_ZONE)
|
149
|
+
G_MONTH_DAY = build(/--([0-9]{2})-([0-9]{2})/, PatternFragments::TIME_ZONE)
|
150
|
+
INTEGER = /\A[+-]?[0-9]+\z/
|
151
|
+
DECIMAL = /\A[+-]?[0-9]+(?:\.[0-9]+)?\z/
|
152
|
+
FLOAT = /\A(?:[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][0-9]+)?|-?INF|NaN)\z/
|
153
|
+
NCNAME = build("(?:#{PatternFragments::NCNAME_START_CHAR})", "(?:#{PatternFragments::NCNAME_CHAR})*")
|
154
|
+
NAME = build("(?:#{PatternFragments::NAME_START_CHAR})", "(?:#{PatternFragments::NAME_CHAR})*")
|
155
|
+
TOKEN = /\A[^\u0020\u000A\u000D\u0009]+(?: [^\u0020\u000A\u000D\u0009]+)*\z/
|
156
|
+
NORMALIZED_STRING = /\A[^\u000A\u000D\u0009]+\z/
|
157
|
+
NMTOKEN = build("(?:#{PatternFragments::NAME_CHAR})+")
|
158
|
+
LANGUAGE = /\A[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*\z/
|
159
|
+
BASE64_BINARY = /\A(?:(?:[A-Za-z0-9+\/] ?){4})*(?:(?:[A-Za-z0-9+\/] ?){3}[A-Za-z0-9+\/]|(?:[A-Za-z0-9+\/] ?){2}[AEIMQUYcgkosw048] ?=|[A-Za-z0-9+\/] ?[AQgw] ?= ?=)?\z/
|
160
|
+
end
|
161
|
+
|
162
|
+
module Convertors
|
163
|
+
ANY_URI = ->(value, item_type) {
|
164
|
+
uri_classes = [URI::Generic]
|
165
|
+
case value
|
166
|
+
when URI::Generic
|
167
|
+
value.to_s
|
168
|
+
else
|
169
|
+
begin
|
170
|
+
URI(value.to_s).to_s
|
171
|
+
rescue URI::InvalidURIError
|
172
|
+
raise Errors::BadRubyValue.new(value, item_type)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
}
|
176
|
+
BASE64_BINARY = ->(value, item_type) {
|
177
|
+
Base64.strict_encode64(value.to_s.force_encoding(Encoding::ASCII_8BIT))
|
178
|
+
}
|
179
|
+
BOOLEAN = ->(value, item_type) {
|
180
|
+
value ? 'true' : 'false'
|
181
|
+
}
|
182
|
+
BYTE = ->(value, item_type) {
|
183
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type) if value.bytesize != 1
|
184
|
+
value = value.to_s.force_encoding(Encoding::ASCII_8BIT)
|
185
|
+
value.unpack('c').first.to_s
|
186
|
+
}
|
187
|
+
DATE = ->(value, item_type) {
|
188
|
+
if value.respond_to?(:strftime)
|
189
|
+
value.strftime('%F')
|
190
|
+
else
|
191
|
+
LexicalStringConversion.validate(value, item_type, Patterns::DATE)
|
192
|
+
end
|
193
|
+
}
|
194
|
+
DATE_TIME = ->(value, item_type) {
|
195
|
+
if value.respond_to?(:strftime)
|
196
|
+
value.strftime('%FT%T%:z')
|
197
|
+
else
|
198
|
+
LexicalStringConversion.validate(value, item_type, Patterns::DATE_TIME)
|
199
|
+
end
|
200
|
+
}
|
201
|
+
TIME = ->(value, item_type) {
|
202
|
+
LexicalStringConversion.validate(value, item_type, Patterns::TIME)
|
203
|
+
}
|
204
|
+
DATE_TIME_STAMP = DATE_TIME
|
205
|
+
DAY_TIME_DURATION = ->(value, item_type) {
|
206
|
+
case value
|
207
|
+
when Integer
|
208
|
+
sign = value.negative? ? '-' : ''
|
209
|
+
"#{sign}PT#{value.abs}S"
|
210
|
+
when BigDecimal
|
211
|
+
sign = value.negative? ? '-' : ''
|
212
|
+
"#{sign}PT#{value.abs.to_s('F')}S"
|
213
|
+
when Numeric
|
214
|
+
sign = value.negative? ? '-' : ''
|
215
|
+
sprintf("%sPT%0.9fS", sign, value.abs)
|
216
|
+
else
|
217
|
+
LexicalStringConversion.validate(value, item_type, Patterns::DAY_TIME_DURATION)
|
218
|
+
end
|
219
|
+
}
|
220
|
+
DECIMAL = ->(value, item_type) {
|
221
|
+
case value
|
222
|
+
when ::Integer
|
223
|
+
value.to_s
|
224
|
+
when ::BigDecimal
|
225
|
+
value.to_s('F')
|
226
|
+
when ::Float
|
227
|
+
BigDecimal(value, ::Float::DIG).to_s('F')
|
228
|
+
else
|
229
|
+
LexicalStringConversion.validate(value, item_type, Patterns::DECIMAL)
|
230
|
+
end
|
231
|
+
}
|
232
|
+
DOUBLE = FloatConversion.new(:single)
|
233
|
+
DURATION = ->(value, item_type) {
|
234
|
+
case value
|
235
|
+
when Integer
|
236
|
+
sign = value.negative? ? '-' : ''
|
237
|
+
"#{sign}PT#{value.abs}S"
|
238
|
+
when BigDecimal
|
239
|
+
sign = value.negative? ? '-' : ''
|
240
|
+
"#{sign}PT#{value.abs.to_s('F')}S"
|
241
|
+
when Numeric
|
242
|
+
sign = value.negative? ? '-' : ''
|
243
|
+
sprintf("%sPT%0.9fS", sign, value.abs)
|
244
|
+
else
|
245
|
+
LexicalStringConversion.validate(value, item_type, Patterns::DURATION)
|
246
|
+
end
|
247
|
+
}
|
248
|
+
FLOAT = FloatConversion.new
|
249
|
+
G_DAY = GDateConversion.new({
|
250
|
+
bounds: 1..31,
|
251
|
+
validation_pattern: Patterns::G_DAY,
|
252
|
+
integer_formatter: ->(value) { '---%02d' }
|
253
|
+
})
|
254
|
+
G_MONTH = GDateConversion.new({
|
255
|
+
bounds: 1..12,
|
256
|
+
validation_pattern: Patterns::G_MONTH,
|
257
|
+
integer_formatter: ->(value) { '--%02d' }
|
258
|
+
})
|
259
|
+
G_MONTH_DAY = ->(value, item_type) {
|
260
|
+
month_days = {
|
261
|
+
1 => 31,
|
262
|
+
2 => 29,
|
263
|
+
3 => 31,
|
264
|
+
4 => 30,
|
265
|
+
5 => 31,
|
266
|
+
6 => 30,
|
267
|
+
7 => 31,
|
268
|
+
8 => 31,
|
269
|
+
9 => 30,
|
270
|
+
10 => 31,
|
271
|
+
11 => 30,
|
272
|
+
12 => 31
|
273
|
+
}
|
274
|
+
formatted_value = LexicalStringConversion.validate(value, item_type, Patterns::G_MONTH_DAY)
|
275
|
+
month, day = Patterns::G_MONTH_DAY.match(formatted_value).captures.take(2).map { |i|
|
276
|
+
Integer(i, 10)
|
277
|
+
}
|
278
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type) if day > month_days[month]
|
279
|
+
formatted_value
|
280
|
+
}
|
281
|
+
G_YEAR = GDateConversion.new({
|
282
|
+
bounds: ->(value) { value != 0 },
|
283
|
+
validation_pattern: Patterns::G_YEAR,
|
284
|
+
integer_formatter: ->(value) {
|
285
|
+
value.negative? ? '%05d' : '%04d'
|
286
|
+
}
|
287
|
+
})
|
288
|
+
G_YEAR_MONTH = ->(value, item_type) {
|
289
|
+
formatted_value = LexicalStringConversion.validate(value, item_type, Patterns::G_YEAR_MONTH)
|
290
|
+
year, month = Patterns::G_YEAR_MONTH.match(formatted_value).captures.take(2).map { |i|
|
291
|
+
Integer(i, 10)
|
292
|
+
}
|
293
|
+
if year == 0 || !(1..12).include?(month)
|
294
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type)
|
295
|
+
end
|
296
|
+
value
|
297
|
+
}
|
298
|
+
HEX_BINARY = ->(value, item_type) {
|
299
|
+
value.to_s.force_encoding(Encoding::ASCII_8BIT).each_byte.map { |b| b.to_s(16) }.join
|
300
|
+
}
|
301
|
+
INT = IntegerConversion.new(-2147483648, 2147483647)
|
302
|
+
INTEGER = IntegerConversion.new(nil, nil)
|
303
|
+
LANGUAGE = ->(value, item_type) {
|
304
|
+
LexicalStringConversion.validate(value, item_type, Patterns::LANGUAGE)
|
305
|
+
}
|
306
|
+
LONG = IntegerConversion.new(-9223372036854775808, 9223372036854775807)
|
307
|
+
NAME = ->(value, item_type) {
|
308
|
+
LexicalStringConversion.validate(value, item_type, Patterns::NAME)
|
309
|
+
}
|
310
|
+
ID = IDREF = ENTITY = NCNAME = ->(value, item_type) {
|
311
|
+
LexicalStringConversion.validate(value, item_type, Patterns::NCNAME)
|
312
|
+
}
|
313
|
+
NEGATIVE_INTEGER = IntegerConversion.new(nil, -1)
|
314
|
+
NMTOKEN = ->(value, item_type) {
|
315
|
+
LexicalStringConversion.validate(value, item_type, Patterns::NMTOKEN)
|
316
|
+
}
|
317
|
+
NON_NEGATIVE_INTEGER = IntegerConversion.new(0, nil)
|
318
|
+
NON_POSITIVE_INTEGER = IntegerConversion.new(nil, 0)
|
319
|
+
NORMALIZED_STRING = ->(value, item_type) {
|
320
|
+
LexicalStringConversion.validate(value, item_type, Patterns::NORMALIZED_STRING)
|
321
|
+
}
|
322
|
+
POSITIVE_INTEGER = IntegerConversion.new(1, nil)
|
323
|
+
SHORT = IntegerConversion.new(-32768, 32767)
|
324
|
+
# STRING (It's questionable whether anything needs doing here)
|
325
|
+
TOKEN = ->(value, item_type) {
|
326
|
+
LexicalStringConversion.validate(value, item_type, Patterns::TOKEN)
|
327
|
+
}
|
328
|
+
UNSIGNED_BYTE = ->(value, item_type) {
|
329
|
+
raise Errors::RubyValueOutOfBounds.new(value, item_type) if value.bytesize != 1
|
330
|
+
value = value.to_s.force_encoding(Encoding::ASCII_8BIT)
|
331
|
+
value.unpack('C').first.to_s
|
332
|
+
}
|
333
|
+
UNSIGNED_INT = IntegerConversion.new(0, 4294967295)
|
334
|
+
UNSIGNED_LONG = IntegerConversion.new(0, 18446744073709551615)
|
335
|
+
UNSIGNED_SHORT = IntegerConversion.new(0, 65535)
|
336
|
+
YEAR_MONTH_DURATION = ->(value, item_type) {
|
337
|
+
LexicalStringConversion.validate(value, item_type, Patterns::YEAR_MONTH_DURATION)
|
338
|
+
}
|
339
|
+
QNAME = NOTATION = ->(value, item_type) {
|
340
|
+
raise Errors::UnconvertableNamespaceSensitveItemType
|
341
|
+
}
|
342
|
+
end
|
343
|
+
|
344
|
+
module Errors
|
345
|
+
# Raised during conversion from Ruby value to XDM Type lexical string
|
346
|
+
# when the ruby value does not conform to the Type's string
|
347
|
+
# representation.
|
348
|
+
class BadRubyValue < ArgumentError
|
349
|
+
attr_reader :value, :item_type
|
350
|
+
|
351
|
+
def initialize(value, item_type)
|
352
|
+
@value, @item_type = value, item_type
|
353
|
+
end
|
354
|
+
|
355
|
+
def to_s
|
356
|
+
"Ruby value #{value.inspect} cannot be converted to an XDM #{item_type.type_name.to_s}"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Raised during conversion from Ruby value to XDM type lexical string
|
361
|
+
# when the ruby value fits the Type's string representation, but is out
|
362
|
+
# of the permitted bounds for the type, for instance an integer bigger
|
363
|
+
# than <tt>32767</tt> for the type <tt>xs:short</tt>
|
364
|
+
class RubyValueOutOfBounds < ArgumentError
|
365
|
+
attr_reader :value, :item_type
|
366
|
+
|
367
|
+
def initialize(value, item_type)
|
368
|
+
@value, @item_type = value, item_type
|
369
|
+
end
|
370
|
+
|
371
|
+
def to_s
|
372
|
+
"Ruby value #{value.inspect} is outside the allowed bounds of an XDM #{item_type.type_name.to_s}"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Raised during conversion from Ruby value to XDM type if the XDM type
|
377
|
+
# is one of the namespace-sensitive types that cannot be created from a
|
378
|
+
# lexical string
|
379
|
+
class UnconvertableNamespaceSensitveItemType < Exception; end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|