rdf 0.3.3 → 0.3.4
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/README +107 -20
- data/VERSION +1 -1
- data/bin/rdf +4 -8
- data/etc/doap.nt +8 -8
- data/lib/rdf.rb +1 -0
- data/lib/rdf/cli.rb +146 -19
- data/lib/rdf/format.rb +59 -10
- data/lib/rdf/mixin/type_check.rb +21 -0
- data/lib/rdf/model/graph.rb +1 -0
- data/lib/rdf/model/list.rb +36 -3
- data/lib/rdf/model/literal.rb +113 -74
- data/lib/rdf/model/literal/boolean.rb +15 -5
- data/lib/rdf/model/literal/date.rb +24 -6
- data/lib/rdf/model/literal/datetime.rb +21 -4
- data/lib/rdf/model/literal/decimal.rb +3 -128
- data/lib/rdf/model/literal/double.rb +4 -107
- data/lib/rdf/model/literal/integer.rb +3 -97
- data/lib/rdf/model/literal/numeric.rb +178 -3
- data/lib/rdf/model/literal/time.rb +23 -3
- data/lib/rdf/model/literal/token.rb +2 -2
- data/lib/rdf/model/literal/xml.rb +1 -1
- data/lib/rdf/model/node.rb +35 -5
- data/lib/rdf/model/statement.rb +7 -6
- data/lib/rdf/model/term.rb +32 -0
- data/lib/rdf/model/uri.rb +13 -7
- data/lib/rdf/model/value.rb +10 -0
- data/lib/rdf/nquads.rb +120 -3
- data/lib/rdf/ntriples/format.rb +9 -1
- data/lib/rdf/ntriples/writer.rb +1 -0
- data/lib/rdf/query.rb +121 -13
- data/lib/rdf/query/pattern.rb +28 -15
- data/lib/rdf/query/solution.rb +47 -0
- data/lib/rdf/query/solutions.rb +45 -10
- data/lib/rdf/query/variable.rb +39 -4
- data/lib/rdf/reader.rb +47 -4
- data/lib/rdf/repository.rb +8 -4
- data/lib/rdf/util/file.rb +5 -2
- data/lib/rdf/version.rb +1 -1
- data/lib/rdf/writer.rb +34 -5
- metadata +56 -88
data/lib/rdf/format.rb
CHANGED
@@ -19,7 +19,7 @@ module RDF
|
|
19
19
|
# RDF::Format.content_types #=> {"text/plain" => [RDF::NTriples::Format]}
|
20
20
|
#
|
21
21
|
# @example Obtaining serialization format file extension mappings
|
22
|
-
# RDF::Format.file_extensions #=> {:nt =>
|
22
|
+
# RDF::Format.file_extensions #=> {:nt => [RDF::NTriples::Format]}
|
23
23
|
#
|
24
24
|
# @example Defining a new RDF serialization format class
|
25
25
|
# class RDF::NTriples::Format < RDF::Format
|
@@ -77,11 +77,18 @@ module RDF
|
|
77
77
|
# @option options [String, #to_s] :file_name (nil)
|
78
78
|
# @option options [Symbol, #to_sym] :file_extension (nil)
|
79
79
|
# @option options [String, #to_s] :content_type (nil)
|
80
|
+
# Note that content_type will be taken from a URL opened using {RDF::Util::File.open_file}.
|
81
|
+
# @option options [String] :sample (nil)
|
82
|
+
# A sample of input used for performing format detection.
|
83
|
+
# If we find no formats, or we find more than one, and we have a sample, we can
|
84
|
+
# perform format detection to find a specific format to use, in which case
|
85
|
+
# we pick the first one we find
|
80
86
|
# @return [Class]
|
87
|
+
# @yieldreturn [String] another way to provide a sample, allows lazy for retrieving the sample.
|
81
88
|
#
|
82
89
|
# @return [Class]
|
83
90
|
def self.for(options = {})
|
84
|
-
case options
|
91
|
+
format = case options
|
85
92
|
when String
|
86
93
|
# Find a format based on the file name
|
87
94
|
self.for(:file_name => options)
|
@@ -94,15 +101,13 @@ module RDF
|
|
94
101
|
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
95
102
|
mime_type = mime_type.to_s
|
96
103
|
mime_type = mime_type.split(';').first if mime_type.include?(?;) # remove any media type parameters
|
97
|
-
content_types
|
104
|
+
content_types[mime_type]
|
98
105
|
# Find a format based on the file name:
|
99
106
|
when file_name = options[:file_name]
|
100
107
|
self.for(:file_extension => File.extname(file_name.to_s)[1..-1])
|
101
108
|
# Find a format based on the file extension:
|
102
109
|
when file_ext = options[:file_extension]
|
103
|
-
|
104
|
-
self.for(:content_type => file_extensions[file_ext])
|
105
|
-
end
|
110
|
+
file_extensions[file_ext.to_sym]
|
106
111
|
end
|
107
112
|
|
108
113
|
when Symbol
|
@@ -112,15 +117,31 @@ module RDF
|
|
112
117
|
RDF::NTriples::Format
|
113
118
|
# For anything else, find a match based on the full class name
|
114
119
|
else
|
115
|
-
format = format.to_s.downcase
|
116
120
|
@@subclasses.each do |klass|
|
117
|
-
if klass.
|
121
|
+
if klass.to_sym == format ||
|
122
|
+
klass.name.to_s.split('::').map(&:downcase).include?(format.to_s.downcase)
|
118
123
|
return klass
|
119
124
|
end
|
120
125
|
end
|
121
126
|
nil # not found
|
122
127
|
end
|
123
128
|
end
|
129
|
+
|
130
|
+
if format.is_a?(Array)
|
131
|
+
return format.first if format.uniq.length == 1
|
132
|
+
elsif !format.nil?
|
133
|
+
return format
|
134
|
+
end
|
135
|
+
|
136
|
+
# If we have a sample, use that for format detection
|
137
|
+
if sample = (options[:sample] if options.is_a?(Hash)) || (yield if block_given?)
|
138
|
+
# Given a sample, perform format detection across the appropriate formats, choosing
|
139
|
+
# the first that matches
|
140
|
+
format ||= @@subclasses
|
141
|
+
|
142
|
+
# Return first format that has a positive detection
|
143
|
+
format.detect {|f| f.detect(sample)}
|
144
|
+
end
|
124
145
|
end
|
125
146
|
|
126
147
|
##
|
@@ -134,11 +155,21 @@ module RDF
|
|
134
155
|
##
|
135
156
|
# Returns file extensions for known RDF serialization formats.
|
136
157
|
#
|
137
|
-
# @return [Hash{Symbol =>
|
158
|
+
# @return [Hash{Symbol => Array<Class>}]
|
138
159
|
def self.file_extensions
|
139
160
|
@@file_extensions
|
140
161
|
end
|
141
162
|
|
163
|
+
##
|
164
|
+
# Returns a symbol appropriate to use with RDF::Format.for()
|
165
|
+
# @return [Symbol]
|
166
|
+
def self.to_sym
|
167
|
+
elements = self.to_s.split("::")
|
168
|
+
sym = elements.pop
|
169
|
+
sym = elements.pop if sym == 'Format'
|
170
|
+
sym.downcase.to_s.to_sym
|
171
|
+
end
|
172
|
+
|
142
173
|
##
|
143
174
|
# Retrieves or defines the reader class for this RDF serialization
|
144
175
|
# format.
|
@@ -225,6 +256,24 @@ module RDF
|
|
225
256
|
end
|
226
257
|
end
|
227
258
|
|
259
|
+
|
260
|
+
##
|
261
|
+
# Use a text sample to detect the format of an input file. Sub-classes implement
|
262
|
+
# a matcher sufficient to detect probably format matches, including disambiguating
|
263
|
+
# between other similar formats.
|
264
|
+
#
|
265
|
+
# Used to determine format class from loaded formats by {RDF::Format.for} when a
|
266
|
+
# match cannot be unambigiously found otherwise.
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# RDF::NTriples::Format.detect("<a> <b> <c> .") => true
|
270
|
+
#
|
271
|
+
# @param [String] sample Beginning several bytes (~ 1K) of input.
|
272
|
+
# @return [Boolean]
|
273
|
+
def self.detect(sample)
|
274
|
+
false
|
275
|
+
end
|
276
|
+
|
228
277
|
class << self
|
229
278
|
alias_method :reader_class, :reader
|
230
279
|
alias_method :writer_class, :writer
|
@@ -267,7 +316,7 @@ module RDF
|
|
267
316
|
|
268
317
|
if extensions = (options[:extension] || options[:extensions])
|
269
318
|
extensions = [extensions].flatten.map(&:to_sym)
|
270
|
-
extensions.each { |ext| @@file_extensions[ext]
|
319
|
+
extensions.each { |ext| (@@file_extensions[ext] ||= []) << self }
|
271
320
|
end
|
272
321
|
if aliases = (options[:alias] || options[:aliases])
|
273
322
|
aliases = [aliases].flatten.each { |a| (@@content_types[a] ||= []) << self }
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RDF
|
2
|
+
##
|
3
|
+
# An RDF type check mixin.
|
4
|
+
#
|
5
|
+
# This module implements #raise_error, which will raise RDF::TypeError.
|
6
|
+
#
|
7
|
+
# @see RDF::Value
|
8
|
+
# @see RDF::Literal
|
9
|
+
# @see RDF::Literal
|
10
|
+
module TypeCheck
|
11
|
+
##
|
12
|
+
# Default implementation of type_error, which returns false.
|
13
|
+
# Classes including RDF::TypeCheck will raise TypeError
|
14
|
+
# instead.
|
15
|
+
#
|
16
|
+
# @raise [TypeError]
|
17
|
+
def type_error(message)
|
18
|
+
raise TypeError, message
|
19
|
+
end
|
20
|
+
end # TypeCheck
|
21
|
+
end # RDF
|
data/lib/rdf/model/graph.rb
CHANGED
data/lib/rdf/model/list.rb
CHANGED
@@ -56,17 +56,50 @@ module RDF
|
|
56
56
|
# The canonical empty list.
|
57
57
|
NIL = RDF::List.new(RDF.nil).freeze
|
58
58
|
|
59
|
+
##
|
60
|
+
# Validate the list ensuring that
|
61
|
+
# * rdf:rest values are all BNodes are nil
|
62
|
+
# * rdf:type, if it exists, is rdf:List
|
63
|
+
# * each subject has no properties other than single-valued rdf:first, rdf:rest
|
64
|
+
# other than for the first node in the list
|
65
|
+
# @return [Boolean]
|
66
|
+
def valid?
|
67
|
+
li = subject
|
68
|
+
while li != RDF.nil do
|
69
|
+
rest = nil
|
70
|
+
firsts = rests = 0
|
71
|
+
@graph.query(:subject => li) do |st|
|
72
|
+
case st.predicate
|
73
|
+
when RDF.type
|
74
|
+
# Be tollerant about rdf:type entries, as some OWL vocabularies use it excessively
|
75
|
+
when RDF.first
|
76
|
+
firsts += 1
|
77
|
+
when RDF.rest
|
78
|
+
rest = st.object
|
79
|
+
return false unless rest.node? || rest == RDF.nil
|
80
|
+
rests += 1
|
81
|
+
else
|
82
|
+
# First node may have other properties
|
83
|
+
return false unless li == subject
|
84
|
+
end
|
85
|
+
end
|
86
|
+
return false unless firsts == 1 && rests == 1
|
87
|
+
li = rest
|
88
|
+
end
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
59
92
|
##
|
60
93
|
# Returns the subject term of this list.
|
61
94
|
#
|
62
|
-
# @
|
95
|
+
# @attr_reader [RDF::Resource]
|
63
96
|
attr_reader :subject
|
64
97
|
|
65
98
|
##
|
66
99
|
# Returns the underlying graph storing the statements that constitute
|
67
100
|
# this list.
|
68
101
|
#
|
69
|
-
# @
|
102
|
+
# @attr_reader [RDF::Graph]
|
70
103
|
attr_reader :graph
|
71
104
|
|
72
105
|
##
|
@@ -489,7 +522,7 @@ module RDF
|
|
489
522
|
##
|
490
523
|
# Returns the first subject term constituting this list.
|
491
524
|
#
|
492
|
-
# This is equivalent to
|
525
|
+
# This is equivalent to `subject`.
|
493
526
|
#
|
494
527
|
# @example
|
495
528
|
# RDF::List[1, 2, 3].first_subject #=> RDF::Node(...)
|
data/lib/rdf/model/literal.rb
CHANGED
@@ -2,9 +2,30 @@ module RDF
|
|
2
2
|
##
|
3
3
|
# An RDF literal.
|
4
4
|
#
|
5
|
+
# Subclasses of {RDF::Literal} should define DATATYPE and GRAMMAR constants, which are used
|
6
|
+
# for identifying the appropriate class to use for a datatype URI and to perform lexical
|
7
|
+
# matching on the value.
|
8
|
+
#
|
9
|
+
# Literal comparison with other {RDF::Value} instances call {RDF::Value#type_error},
|
10
|
+
# which, returns false. Implementations wishing to have {RDF::TypeError} raised
|
11
|
+
# should mix-in {RDF::TypeCheck}. This is required for strict SPARQL conformance.
|
12
|
+
#
|
13
|
+
# Specific typed literals may have behavior different from the default implementation. See
|
14
|
+
# the following defined sub-classes for specific documentation. Additional sub-classes may
|
15
|
+
# be defined, and will interoperate by defining `DATATYPE` and `GRAMMAR` constants, in addition
|
16
|
+
# other required overrides of RDF::Literal behavior.
|
17
|
+
#
|
18
|
+
# * {RDF::Literal::Boolean}
|
19
|
+
# * {RDF::Literal::Date}
|
20
|
+
# * {RDF::Literal::DateTime}
|
21
|
+
# * {RDF::Literal::Decimal}
|
22
|
+
# * {RDF::Literal::Double}
|
23
|
+
# * {RDF::Literal::Integer}
|
24
|
+
# * {RDF::Literal::Time}
|
25
|
+
#
|
5
26
|
# @example Creating a plain literal
|
6
27
|
# value = RDF::Literal.new("Hello, world!")
|
7
|
-
# value.plain? #=> true
|
28
|
+
# value.plain? #=> true`
|
8
29
|
#
|
9
30
|
# @example Creating a language-tagged literal (1)
|
10
31
|
# value = RDF::Literal.new("Hello!", :language => :en)
|
@@ -39,53 +60,48 @@ module RDF
|
|
39
60
|
# @see http://www.w3.org/TR/rdf-concepts/#section-Literals
|
40
61
|
# @see http://www.w3.org/TR/rdf-concepts/#section-Datatypes-intro
|
41
62
|
class Literal
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
63
|
+
|
64
|
+
private
|
65
|
+
@@subclasses = [] # @private
|
66
|
+
|
67
|
+
##
|
68
|
+
# @private
|
69
|
+
# @return [void]
|
70
|
+
def self.inherited(child)
|
71
|
+
@@subclasses << child
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
public
|
76
|
+
|
77
|
+
require 'rdf/model/literal/numeric'
|
78
|
+
require 'rdf/model/literal/boolean'
|
79
|
+
require 'rdf/model/literal/decimal'
|
80
|
+
require 'rdf/model/literal/integer'
|
81
|
+
require 'rdf/model/literal/double'
|
82
|
+
require 'rdf/model/literal/date'
|
83
|
+
require 'rdf/model/literal/dateTime'
|
84
|
+
require 'rdf/model/literal/time'
|
85
|
+
require 'rdf/model/literal/token'
|
86
|
+
require 'rdf/model/literal/xml'
|
52
87
|
|
53
88
|
include RDF::Term
|
54
89
|
|
90
|
+
##
|
91
|
+
# @private
|
92
|
+
# Return datatype class for uri, or nil if none is found
|
93
|
+
def self.datatyped_class(uri)
|
94
|
+
@@subclasses.detect {|klass| klass.const_defined?(:DATATYPE) && klass.const_get(:DATATYPE) == uri}
|
95
|
+
end
|
96
|
+
|
55
97
|
##
|
56
98
|
# @private
|
57
99
|
def self.new(value, options = {})
|
58
100
|
klass = case
|
59
101
|
when !self.equal?(RDF::Literal)
|
60
102
|
self # subclasses can be directly constructed without type dispatch
|
61
|
-
when
|
62
|
-
|
63
|
-
when XSD.boolean
|
64
|
-
RDF::Literal::Boolean
|
65
|
-
when XSD.integer, XSD.long, XSD.int, XSD.short, XSD.byte
|
66
|
-
RDF::Literal::Integer
|
67
|
-
when XSD.double, XSD.float
|
68
|
-
RDF::Literal::Double
|
69
|
-
when XSD.decimal
|
70
|
-
RDF::Literal::Decimal
|
71
|
-
when XSD.date
|
72
|
-
RDF::Literal::Date
|
73
|
-
when XSD.dateTime
|
74
|
-
RDF::Literal::DateTime
|
75
|
-
when XSD.time
|
76
|
-
RDF::Literal::Time
|
77
|
-
when XSD.nonPositiveInteger, XSD.negativeInteger
|
78
|
-
RDF::Literal::Integer
|
79
|
-
when XSD.nonNegativeInteger, XSD.positiveInteger
|
80
|
-
RDF::Literal::Integer
|
81
|
-
when XSD.unsignedLong, XSD.unsignedInt, XSD.unsignedShort, XSD.unsignedByte
|
82
|
-
RDF::Literal::Integer
|
83
|
-
when XSD.token, XSD.language
|
84
|
-
RDF::Literal::Token
|
85
|
-
when RDF.XMLLiteral
|
86
|
-
RDF::Literal::XML
|
87
|
-
else self
|
88
|
-
end
|
103
|
+
when typed_literal = datatyped_class(RDF::URI(options[:datatype]))
|
104
|
+
typed_literal
|
89
105
|
else case value
|
90
106
|
when ::TrueClass then RDF::Literal::Boolean
|
91
107
|
when ::FalseClass then RDF::Literal::Boolean
|
@@ -123,8 +139,10 @@ module RDF
|
|
123
139
|
def initialize(value, options = {})
|
124
140
|
@object = value
|
125
141
|
@string = options[:lexical] if options[:lexical]
|
142
|
+
@string = value if !defined?(@string) && value.is_a?(String)
|
126
143
|
@language = options[:language].to_s.to_sym if options[:language]
|
127
144
|
@datatype = RDF::URI(options[:datatype]) if options[:datatype]
|
145
|
+
@datatype ||= self.class.const_get(:DATATYPE) if self.class.const_defined?(:DATATYPE)
|
128
146
|
end
|
129
147
|
|
130
148
|
##
|
@@ -138,30 +156,7 @@ module RDF
|
|
138
156
|
##
|
139
157
|
# @return [Object]
|
140
158
|
def object
|
141
|
-
@object
|
142
|
-
when XSD.string, nil
|
143
|
-
value
|
144
|
-
when XSD.boolean
|
145
|
-
%w(true 1).include?(value)
|
146
|
-
when XSD.integer, XSD.long, XSD.int, XSD.short, XSD.byte
|
147
|
-
value.to_i
|
148
|
-
when XSD.double, XSD.float
|
149
|
-
value.to_f
|
150
|
-
when XSD.decimal
|
151
|
-
::BigDecimal.new(value)
|
152
|
-
when XSD.date
|
153
|
-
::Date.parse(value)
|
154
|
-
when XSD.dateTime
|
155
|
-
::DateTime.parse(value)
|
156
|
-
when XSD.time
|
157
|
-
::Time.parse(value)
|
158
|
-
when XSD.nonPositiveInteger, XSD.negativeInteger
|
159
|
-
value.to_i
|
160
|
-
when XSD.nonNegativeInteger, XSD.positiveInteger
|
161
|
-
value.to_i
|
162
|
-
when XSD.unsignedLong, XSD.unsignedInt, XSD.unsignedShort, XSD.unsignedByte
|
163
|
-
value.to_i
|
164
|
-
end
|
159
|
+
defined?(@object) ? @object : value
|
165
160
|
end
|
166
161
|
|
167
162
|
##
|
@@ -189,7 +184,7 @@ module RDF
|
|
189
184
|
end
|
190
185
|
|
191
186
|
##
|
192
|
-
#
|
187
|
+
# Determins if `self` is the same term as `other`.
|
193
188
|
#
|
194
189
|
# @example
|
195
190
|
# RDF::Literal(1).eql?(RDF::Literal(1.0)) #=> false
|
@@ -199,27 +194,43 @@ module RDF
|
|
199
194
|
def eql?(other)
|
200
195
|
self.equal?(other) ||
|
201
196
|
(self.class.eql?(other.class) &&
|
202
|
-
self.
|
203
|
-
self
|
197
|
+
self.value.eql?(other.value) &&
|
198
|
+
self.language.to_s.downcase.eql?(other.language.to_s.downcase) &&
|
199
|
+
self.datatype.eql?(other.datatype))
|
204
200
|
end
|
205
201
|
|
206
202
|
##
|
207
|
-
# Returns `true` if this literal is equivalent to `other
|
203
|
+
# Returns `true` if this literal is equivalent to `other` (with type check).
|
208
204
|
#
|
209
205
|
# @example
|
210
206
|
# RDF::Literal(1) == RDF::Literal(1.0) #=> true
|
211
207
|
#
|
212
208
|
# @param [Object] other
|
213
209
|
# @return [Boolean] `true` or `false`
|
210
|
+
#
|
211
|
+
# @see http://www.w3.org/TR/rdf-sparql-query/#func-RDFterm-equal
|
212
|
+
# @see http://www.w3.org/TR/rdf-concepts/#section-Literal-Equality
|
214
213
|
def ==(other)
|
215
214
|
case other
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
when
|
221
|
-
|
222
|
-
|
215
|
+
when Literal
|
216
|
+
case
|
217
|
+
when self.eql?(other)
|
218
|
+
true
|
219
|
+
when self.has_language? && self.language.to_s.downcase == other.language.to_s.downcase
|
220
|
+
# Literals with languages can compare if languages are identical
|
221
|
+
self.value == other.value
|
222
|
+
when (self.simple? || self.datatype == XSD.string) && (other.simple? || other.datatype == XSD.string)
|
223
|
+
self.value == other.value
|
224
|
+
when other.comperable_datatype?(self) || self.comperable_datatype?(other)
|
225
|
+
# Comoparing plain with undefined datatypes does not generate an error, but returns false
|
226
|
+
# From data-r2/expr-equal/eq-2-2.
|
227
|
+
false
|
228
|
+
else
|
229
|
+
type_error("unable to determine whether #{self.inspect} and #{other.inspect} are equivalent")
|
230
|
+
end
|
231
|
+
when String
|
232
|
+
self.plain? && self.value.eql?(other)
|
233
|
+
else false
|
223
234
|
end
|
224
235
|
end
|
225
236
|
alias_method :===, :==
|
@@ -277,6 +288,34 @@ module RDF
|
|
277
288
|
!valid?
|
278
289
|
end
|
279
290
|
|
291
|
+
##
|
292
|
+
# Returns `true` if the literal has a datatype and the comparison should
|
293
|
+
# return false instead of raise a type error.
|
294
|
+
#
|
295
|
+
# This behavior is intuited from SPARQL data-r2/expr-equal/eq-2-2
|
296
|
+
# @return [Boolean]
|
297
|
+
def comperable_datatype?(other)
|
298
|
+
return false unless self.plain? || self.has_language?
|
299
|
+
|
300
|
+
case other
|
301
|
+
when RDF::Literal::Numeric, RDF::Literal::Boolean,
|
302
|
+
RDF::Literal::Date, RDF::Literal::Time, RDF::Literal::DateTime
|
303
|
+
# Invald types can be compared without raising a TypeError if literal has a language (open-eq-08)
|
304
|
+
!other.valid? && self.has_language?
|
305
|
+
else
|
306
|
+
case other.datatype
|
307
|
+
when XSD.string
|
308
|
+
true
|
309
|
+
when nil
|
310
|
+
# A different language will not generate a type error
|
311
|
+
other.has_language?
|
312
|
+
else
|
313
|
+
# An unknown datatype may not be used for comparison, unless it has a language? (open-eq-8)
|
314
|
+
self.has_language?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
280
319
|
##
|
281
320
|
# Validates the value using {#valid?}, raising an error if the value is
|
282
321
|
# invalid.
|