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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +62 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +43 -0
  10. data/Rakefile +20 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/net/sf/saxon/Saxon-HE/9.9.1-5/Saxon-HE-9.9.1-5.jar +0 -0
  14. data/lib/saxon.rb +6 -0
  15. data/lib/saxon/axis_iterator.rb +31 -0
  16. data/lib/saxon/configuration.rb +116 -0
  17. data/lib/saxon/document_builder.rb +28 -0
  18. data/lib/saxon/item_type.rb +290 -0
  19. data/lib/saxon/item_type/lexical_string_conversion.rb +383 -0
  20. data/lib/saxon/item_type/value_to_ruby.rb +78 -0
  21. data/lib/saxon/jaxp.rb +8 -0
  22. data/lib/saxon/loader.rb +93 -0
  23. data/lib/saxon/occurrence_indicator.rb +33 -0
  24. data/lib/saxon/parse_options.rb +127 -0
  25. data/lib/saxon/processor.rb +102 -0
  26. data/lib/saxon/qname.rb +153 -0
  27. data/lib/saxon/s9api.rb +34 -0
  28. data/lib/saxon/serializer.rb +143 -0
  29. data/lib/saxon/source.rb +187 -0
  30. data/lib/saxon/version.rb +3 -0
  31. data/lib/saxon/xdm.rb +35 -0
  32. data/lib/saxon/xdm/array.rb +77 -0
  33. data/lib/saxon/xdm/atomic_value.rb +173 -0
  34. data/lib/saxon/xdm/empty_sequence.rb +37 -0
  35. data/lib/saxon/xdm/external_object.rb +21 -0
  36. data/lib/saxon/xdm/function_item.rb +21 -0
  37. data/lib/saxon/xdm/item.rb +32 -0
  38. data/lib/saxon/xdm/map.rb +77 -0
  39. data/lib/saxon/xdm/node.rb +71 -0
  40. data/lib/saxon/xdm/sequence_like.rb +30 -0
  41. data/lib/saxon/xdm/value.rb +145 -0
  42. data/lib/saxon/xpath.rb +8 -0
  43. data/lib/saxon/xpath/compiler.rb +69 -0
  44. data/lib/saxon/xpath/executable.rb +58 -0
  45. data/lib/saxon/xpath/static_context.rb +161 -0
  46. data/lib/saxon/xpath/variable_declaration.rb +68 -0
  47. data/lib/saxon/xslt.rb +8 -0
  48. data/lib/saxon/xslt/compiler.rb +70 -0
  49. data/lib/saxon/xslt/evaluation_context.rb +165 -0
  50. data/lib/saxon/xslt/executable.rb +156 -0
  51. data/lib/saxon_jars.rb +10 -0
  52. data/saxon-rb.gemspec +39 -0
  53. data/saxon.gemspec +30 -0
  54. 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