Skip to content

Commit 20454d3

Browse files
committed
Support parsing VERSION with minimal checks.
1 parent 0e3b8aa commit 20454d3

File tree

9 files changed

+132
-36
lines changed

9 files changed

+132
-36
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ group :development, :test do
1616
gem 'rdf-vocab', git: "https://github.com/ruby-rdf/rdf-vocab", branch: "develop"
1717
gem 'rdf-xsd', git: "https://github.com/ruby-rdf/rdf-xsd", branch: "develop"
1818
gem 'sxp', git: "https://github.com/dryruby/sxp.rb", branch: "develop"
19+
gem 'irb'
1920
gem "redcarpet", platforms: :ruby
2021
gem 'simplecov', '~> 0.22', platforms: :mri
2122
gem 'simplecov-lcov', '~> 0.8', platforms: :mri

etc/turtle.bnf

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
turtleDoc ::= statement*
22
statement ::= directive | triples '.'
3-
directive ::= prefixID | base | sparqlPrefix | sparqlBase
3+
directive ::= prefixID | base | version | sparqlPrefix | sparqlBase | sparqlVersion
44
prefixID ::= '@prefix' PNAME_NS IRIREF '.'
55
base ::= '@base' IRIREF '.'
6+
version ::= '@version' VersionSpecifier '.'
67
sparqlPrefix ::= "PREFIX" PNAME_NS IRIREF
78
sparqlBase ::= "BASE" IRIREF
9+
sparqlVersion ::= "VERSION" VersionSpecifier
10+
VersionSpecifier ::= STRING_LITERAL_QUOTE | STRING_LITERAL_SINGLE_QUOTE
811
triples ::= subject predicateObjectList
912
| blankNodePropertyList predicateObjectList?
1013
| reifiedTriple predicateObjectList?

examples/version.ttl

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
VERSION "1.2"
2+
@version "1.1" .
3+
<a> a <b> .

lib/rdf/turtle/reader.rb

+52-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,21 @@ class Reader < RDF::Reader
3838

3939
terminal(:PREFIX, PREFIX)
4040
terminal(:BASE, BASE)
41+
terminal(:VERSION, RDF_VERSION)
4142
terminal(:LANG_DIR, LANG_DIR)
4243

44+
##
45+
# Returns the RDF version determined by this reader.
46+
#
47+
# @example
48+
# reader.version #=> RDF::URI('http://purl.org/dc/terms/')
49+
#
50+
# @return [String]
51+
# @since 3.3.2
52+
def version
53+
@options[:version]
54+
end
55+
4356
##
4457
# Reader options
4558
# @see https://ruby-rdf.github.io/rdf/RDF/Reader#options-class_method
@@ -50,6 +63,12 @@ def self.options
5063
datatype: TrueClass,
5164
on: ["--freebase"],
5265
description: "Use optimized Freebase reader.") {true},
66+
RDF::CLI::Option.new(
67+
symbol: :version,
68+
control: :select,
69+
datatype: %w{1.1 1.2 1.2-basic},
70+
on: ["--version"],
71+
description: "RDF Version."),
5372
]
5473
end
5574

@@ -90,6 +109,8 @@ def self.new(input = nil, **options, &block)
90109
# Record error/info/debug output
91110
# @option options [Boolean] :freebase (false)
92111
# Use optimized Freebase reader
112+
# @option options [String] :version ("1.2")
113+
# Parse a specific version of RDF ("1.1', "1.2", or "1.2-basic"")
93114
# @return [RDF::Turtle::Reader]
94115
def initialize(input = nil, **options, &block)
95116
super do
@@ -259,7 +280,7 @@ def read_statement
259280
prod(:statement, %w{.}) do
260281
error("read_statement", "Unexpected end of file") unless token = @lexer.first
261282
case token.type
262-
when :BASE, :PREFIX
283+
when :BASE, :PREFIX, :VERSION
263284
read_directive || error("Failed to parse directive", production: :directive, token: token)
264285
else
265286
read_triples || error("Expected token", production: :statement, token: token)
@@ -277,7 +298,8 @@ def read_statement
277298
##
278299
# Read directive
279300
#
280-
# directive ::= prefixID | base | sparqlPrefix | sparqlBase
301+
# directive ::= prefixID | base | version
302+
# | sparqlPrefix | sparqlBase | sparqlVersion
281303
#
282304
# @return [void]
283305
def read_directive
@@ -322,6 +344,26 @@ def read_directive
322344
true
323345
end
324346
end
347+
when :VERSION
348+
prod(:version) do
349+
@lexer.shift
350+
terminated = token.value == '@version'
351+
@options[:version] = @lexer.shift.value[1..-2]
352+
if %w(1.2).include?(@options[:version])
353+
progress("version") {@options[:version]}
354+
else
355+
warn("version", "Expected version to be 1.2, was #{@options[:version]}") unless @options[:version] == "1.2"
356+
end
357+
358+
if terminated
359+
error("version", "Expected #{token} to be terminated") unless @lexer.first === '.'
360+
@lexer.shift
361+
elsif @lexer.first === '.'
362+
error("version", "Expected #{token} not to be terminated")
363+
else
364+
true
365+
end
366+
end
325367
end
326368
end
327369
end
@@ -755,6 +797,14 @@ def debug(*args, &block)
755797
log_debug(*args, **opts, &block)
756798
end
757799

800+
def warn(*args, &block)
801+
lineno = (options[:token].lineno if options[:token].respond_to?(:lineno)) || (@lexer && @lexer.lineno)
802+
opts = args.last.is_a?(Hash) ? args.pop : {}
803+
opts[:level] ||= 0
804+
opts[:lineno] ||= lineno
805+
log_warn(*args, **opts, &block)
806+
end
807+
758808
##
759809
# Error information, used as level `0` debug messages.
760810
#

lib/rdf/turtle/terminals.rb

+1-27
Original file line numberDiff line numberDiff line change
@@ -15,63 +15,37 @@ module Terminals
1515
U_CHARS2 = Regexp.compile("\\u00B7|[\\u0300-\\u036F]|[\\u203F-\\u2040]", Regexp::FIXEDENCODING).freeze
1616
IRI_RANGE = Regexp.compile("[[^<>\"{}|^`\\\\]&&[^\\x00-\\x20]]", Regexp::FIXEDENCODING).freeze
1717

18-
# 26
1918
UCHAR = EBNF::LL1::Lexer::UCHAR
20-
# 170s
2119
PERCENT = /%[0-9A-Fa-f]{2}/u.freeze
22-
# 172s
2320
PN_LOCAL_ESC = /\\[_~\.\-\!$\&'\(\)\*\+,;=\/\?\#@%]/u.freeze
24-
# 169s
2521
PLX = /#{PERCENT}|#{PN_LOCAL_ESC}/u.freeze
26-
# 163s
2722
PN_CHARS_BASE = /[A-Z]|[a-z]|#{U_CHARS1}/u.freeze
28-
# 164s
2923
PN_CHARS_U = /_|#{PN_CHARS_BASE}/u.freeze
30-
# 166s
3124
PN_CHARS = /-|[0-9]|#{PN_CHARS_U}|#{U_CHARS2}/u.freeze
3225
PN_LOCAL_BODY = /(?:(?:\.|:|#{PN_CHARS}|#{PLX})*(?:#{PN_CHARS}|:|#{PLX}))?/u.freeze
3326
PN_CHARS_BODY = /(?:(?:\.|#{PN_CHARS})*#{PN_CHARS})?/u.freeze
34-
# 167s
3527
PN_PREFIX = /#{PN_CHARS_BASE}#{PN_CHARS_BODY}/u.freeze
36-
# 168s
3728
PN_LOCAL = /(?:[0-9]|:|#{PN_CHARS_U}|#{PLX})#{PN_LOCAL_BODY}/u.freeze
38-
# 154s
3929
EXPONENT = /[eE][+-]?[0-9]+/u.freeze
40-
# 159s
4130
ECHAR = /\\[tbnrf\\"']/u.freeze
42-
# 18
4331
IRIREF = /<(?:#{IRI_RANGE}|#{UCHAR})*>/u.freeze
44-
# 139s
4532
PNAME_NS = /#{PN_PREFIX}?:/u.freeze
46-
# 140s
4733
PNAME_LN = /#{PNAME_NS}#{PN_LOCAL}/u.freeze
48-
# 141s
4934
BLANK_NODE_LABEL = /_:(?:[0-9]|#{PN_CHARS_U})(?:(?:#{PN_CHARS}|\.)*#{PN_CHARS})?/u.freeze
50-
# 144s
5135
LANG_DIR = /@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*(?:--[a-zA-Z]+)?)/u.freeze
52-
# 19
5336
INTEGER = /[+-]?[0-9]+/u.freeze
54-
# 20
5537
DECIMAL = /[+-]?(?:[0-9]*\.[0-9]+)/u.freeze
56-
# 21
5738
DOUBLE = /[+-]?(?:[0-9]+\.[0-9]*#{EXPONENT}|\.?[0-9]+#{EXPONENT})/u.freeze
58-
# 22
5939
STRING_LITERAL_SINGLE_QUOTE = /'(?:[^\'\\\n\r]|#{ECHAR}|#{UCHAR})*'/u.freeze
60-
# 23
6140
STRING_LITERAL_QUOTE = /"(?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*"/u.freeze
62-
# 24
6341
STRING_LITERAL_LONG_SINGLE_QUOTE = /'''(?:(?:'|'')?(?:[^'\\]|#{ECHAR}|#{UCHAR}))*'''/um.freeze
64-
# 25
6542
STRING_LITERAL_LONG_QUOTE = /"""(?:(?:"|"")?(?:[^"\\]|#{ECHAR}|#{UCHAR}))*"""/um.freeze
6643

67-
# 161s
6844
WS = /(?:\s|(?:#[^\n\r]*))+/um.freeze
69-
# 162s
7045
ANON = /\[#{WS}*\]/um.freeze
71-
# 28t
7246
PREFIX = /@?prefix/ui.freeze
73-
# 29t
7447
BASE = /@?base/ui.freeze
48+
RDF_VERSION = /@?version/ui.freeze
7549

7650
end
7751
end

rdf-turtle.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Gem::Specification.new do |gem|
3434
gem.add_development_dependency 'erubis', '~> 2.7'
3535
gem.add_development_dependency 'getoptlong', '~> 0.2'
3636
gem.add_development_dependency 'htmlentities', '~> 4.3'
37+
gem.add_runtime_dependency 'readline', '~> 0.0'
3738
gem.add_development_dependency 'rspec', '~> 3.12'
3839
gem.add_development_dependency 'rspec-its', '~> 1.3'
3940
gem.add_development_dependency 'rdf-isomorphic', '~> 3.3'

script/parse

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ end
1919
require 'rdf/ntriples'
2020
require 'ebnf/ll1/parser'
2121
require 'getoptlong'
22+
require 'amazing_print'
2223

2324
def run(input, **options)
2425
if options[:profile]
@@ -106,7 +107,6 @@ OPT_ARGS = [
106107
["--errors", GetoptLong::NO_ARGUMENT, "Display invalid statements"],
107108
["--execute", "-e", GetoptLong::REQUIRED_ARGUMENT, "Run against source in argument"],
108109
["--format", GetoptLong::REQUIRED_ARGUMENT, "Output format, any RDF format symbol, sxp, or inspect"],
109-
["--freebase", GetoptLong::NO_ARGUMENT, "Use Freebase reader (obsolete)"],
110110
["--help", "-?", GetoptLong::NO_ARGUMENT, "print this message"],
111111
["--input-format", GetoptLong::REQUIRED_ARGUMENT, "Format of the input file, defaults to ttl"],
112112
["--info", GetoptLong::NO_ARGUMENT, "Show progress on execution"],
@@ -118,6 +118,7 @@ OPT_ARGS = [
118118
["--uri", GetoptLong::REQUIRED_ARGUMENT, "Default base URI"],
119119
["--validate", GetoptLong::NO_ARGUMENT, "Run parser in strict validation mode"],
120120
["--verbose", GetoptLong::NO_ARGUMENT, "Verbose output"],
121+
["--version", GetoptLong::REQUIRED_ARGUMENT, "Version of RDF (1.1, 1.2, or 1.2-basic)"],
121122
]
122123

123124
def usage
@@ -143,14 +144,13 @@ opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]})
143144

144145
opts.each do |opt, arg|
145146
case opt
146-
when '--debug' then logger.level = Logger::DEBUG
147147
when '--uri' then parser_options[:base_uri] = writer_options[:base_uri] = arg
148148
when '--benchmark' then options[:benchmark] = true
149149
when '--canonicalize' then parser_options[:canonicalize] = true
150+
when '--debug' then logger.level = Logger::DEBUG
150151
when '--errors' then options[:errors] = true
151152
when '--execute' then input = arg
152153
when '--format' then options[:output_format] = arg.to_sym
153-
when '--freebase' then parser_options[:freebase] = true
154154
when "--help" then usage()
155155
when '--info' then logger.level = Logger::INFO
156156
when '--input-format' then options[:input_format] = arg.to_sym
@@ -163,6 +163,7 @@ opts.each do |opt, arg|
163163
when '--stream' then writer_options[:stream] = true
164164
when '--validate' then parser_options[:validate] = true
165165
when '--verbose' then $verbose = true
166+
when '--version' then writer_options[:version] = parser_options[:version] = arg
166167
end
167168
end
168169

script/tc

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require 'rdf/isomorphic'
88
require File.expand_path(File.join(File.dirname(__FILE__), "..", 'spec', 'spec_helper'))
99
require File.expand_path(File.join(File.dirname(__FILE__), "..", 'spec', 'suite_helper'))
1010
require 'getoptlong'
11+
require 'amazing_print'
1112

1213
ASSERTOR = "https://greggkellogg.net/foaf#me"
1314
RUN_TIME = Time.now
@@ -74,7 +75,6 @@ def run_tc(tc, **options)
7475
begin
7576
graph << reader
7677
rescue Exception => e
77-
#require 'byebug'; byebug
7878
STDERR.puts "Unexpected exception: #{e.inspect}" if options[:verbose]
7979
result = "failed"
8080
end

spec/reader_spec.rb

+65-2
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,70 @@
647647
end
648648
end
649649
end
650-
650+
651+
describe "@prefix" do
652+
it "sets RDF version from option" do
653+
inner = double("inner")
654+
expect(inner).to receive(:called)
655+
RDF::Turtle::Reader.new(%{<a> <b> <c> .}, version: "1.2") do |reader|
656+
inner.called(reader.class)
657+
expect(reader.version).to eq "1.2"
658+
end
659+
end
660+
661+
it "sets RDF version from @version" do
662+
inner = double("inner")
663+
expect(inner).to receive(:called)
664+
RDF::Turtle::Reader.new(%{@version "1.2" . <a> <b> <c> .}) do |reader|
665+
inner.called(reader.class)
666+
expect(reader.statements.count).to eq 1
667+
expect(reader.version).to eq "1.2"
668+
end
669+
end
670+
671+
it "sets RDF version from VERSION" do
672+
inner = double("inner")
673+
expect(inner).to receive(:called)
674+
RDF::Turtle::Reader.new(%{VERSION "1.2" <a> <b> <c> .}) do |reader|
675+
inner.called(reader.class)
676+
expect(reader.statements.count).to eq 1
677+
expect(reader.version).to eq "1.2"
678+
end
679+
end
680+
681+
it "sets RDF version from version" do
682+
inner = double("inner")
683+
expect(inner).to receive(:called)
684+
RDF::Turtle::Reader.new(%{version "1.2" <a> <b> <c> .}) do |reader|
685+
inner.called(reader.class)
686+
expect(reader.statements.count).to eq 1
687+
expect(reader.version).to eq "1.2"
688+
end
689+
end
690+
691+
it "sets RDF version from version (single quotes)" do
692+
inner = double("inner")
693+
expect(inner).to receive(:called)
694+
RDF::Turtle::Reader.new(%{version '1.2' <a> <b> <c> .}) do |reader|
695+
inner.called(reader.class)
696+
expect(reader.statements.count).to eq 1
697+
expect(reader.version).to eq "1.2"
698+
end
699+
end
700+
701+
it "warns if version is not 1.2" do
702+
logger = RDF::Spec.logger
703+
inner = double("inner")
704+
expect(inner).to receive(:called)
705+
RDF::Turtle::Reader.new(%{@version "1.1" . <a> <b> <c> .}, logger: logger) do |reader|
706+
inner.called(reader.class)
707+
expect(reader.statements.count).to eq 1
708+
expect(reader.version).to eq "1.1"
709+
expect(logger.to_s).to include('Expected version to be 1.2, was 1.1')
710+
end
711+
end
712+
end
713+
651714
describe "BNodes" do
652715
it "should create BNode for identifier with '_' prefix" do
653716
ttl = %(@prefix a: <http://foo/a#> . _:a a:p a:v .)
@@ -1712,7 +1775,7 @@
17121775
subject {
17131776
RDF::Graph.load(doap_nt, format: :ttl)
17141777
}
1715-
it "parses test file" do
1778+
it "parses DOAP file" do
17161779
expect(subject.count).to eq 23
17171780
end
17181781
end

0 commit comments

Comments
 (0)