Skip to content

Commit baa77a0

Browse files
committed
Add format for shex with CLI for matching shapes.
1 parent d0f79fd commit baa77a0

File tree

9 files changed

+217
-25
lines changed

9 files changed

+217
-25
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ The `#initialize` method is called when {ShEx::Algebra::Schema#execute} starts a
164164

165165
To make sure your extension is found, make sure to require it before the shape is executed.
166166

167+
## Command Line
168+
When the `linkeddata` gem is installed, RDF.rb includes a `rdf` executable which acts as a wrapper to perform a number of different
169+
operations on RDF files, including ShEx. The commands specific to ShEx is
170+
171+
* `shex`: Validate repository given shape
172+
173+
Using this command requires either a `shex-input` where the ShEx schema is URI encoded, or `shex`, which references a URI or file path to the schema. Other required options are `shape` and `focus`.
174+
175+
Example usage:
176+
177+
rdf shex https://raw.githubusercontent.com/ruby-rdf/shex/develop/etc/doap.ttl \
178+
--schema https://raw.githubusercontent.com/ruby-rdf/shex/develop/etc/doap.shex \
179+
--focus http://rubygems.org/gems/shex
180+
167181
## Documentation
168182

169183
<http://rubydoc.info/github/ruby-rdf/shex>

etc/doap.json

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"@context": "http://www.w3.org/ns/shex.jsonld",
33
"type": "Schema",
4+
"start": "http://rubygems.org/gems/shex/DOAP",
45
"shapes": [
56
{
7+
"id": "http://rubygems.org/gems/shex/DOAP",
68
"type": "Shape",
7-
"id": "TestShape",
89
"extra": [
910
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
1011
],
@@ -17,9 +18,7 @@
1718
"valueExpr": {
1819
"type": "NodeConstraint",
1920
"values": [
20-
{
21-
"uri": "http://usefulinc.com/ns/doap#Project"
22-
}
21+
"http://usefulinc.com/ns/doap#Project"
2322
]
2423
}
2524
},
@@ -98,15 +97,12 @@
9897
"valueExpr": {
9998
"type": "NodeConstraint",
10099
"values": [
101-
{
102-
"uri": "http://shex.io/shex-semantics/"
103-
}
100+
"http://shex.io/shex-semantics/"
104101
]
105102
}
106103
}
107104
]
108105
}
109106
}
110107
]
111-
}
112-
108+
}

etc/doap.shex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
BASE <http://rubygems.org/gems/shex/>
12
PREFIX doap: <http://usefulinc.com/ns/doap#>
23
PREFIX dc: <http://purl.org/dc/terms/>
3-
<TestShape> EXTRA a {
4+
5+
start=@<DOAP>
6+
7+
<DOAP> EXTRA a {
48
a [doap:Project];
59

610
# May have either or both of doap:name/doap:description or dc:title/dc:description

etc/doap.ttl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@base <http://rubygems.org/gems/shex> .
12
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
23
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
34
@prefix dc: <http://purl.org/dc/terms/> .

lib/shex.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# @see http://shex.io/shex-semantics/#shexc
55
module ShEx
6+
require 'shex/format'
67
autoload :Algebra, 'shex/algebra'
78
autoload :Meta, 'shex/meta'
89
autoload :Parser, 'shex/parser'

lib/shex/format.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
require 'rdf/format'
2+
3+
module ShEx
4+
##
5+
# ShEx format specification. Note that this format does not define any readers or writers.
6+
#
7+
# @example Obtaining an LD Patch format class
8+
# RDF::Format.for(:shex) #=> LD::Patch::Format
9+
# RDF::Format.for("etc/foaf.shex")
10+
# RDF::Format.for(file_name: "etc/foaf.shex")
11+
# RDF::Format.for(file_extension: "shex")
12+
# RDF::Format.for(content_type: "application/shex")
13+
#
14+
# @see http://www.w3.org/TR/ldpatch/
15+
class Format < RDF::Format
16+
content_type 'application/shex', extension: :shex
17+
content_encoding 'utf-8'
18+
19+
##
20+
# Hash of CLI commands appropriate for this format
21+
# @return [Hash{Symbol => Lambda(Array, Hash)}]
22+
def self.cli_commands
23+
{
24+
shex: {
25+
description: "Validate repository given shape",
26+
help: "shex [--shape Resource] [--focus Resource] [--schema-input STRING] [--schema STRING] file",
27+
parse: true,
28+
lambda: -> (argv, options) do
29+
options[:schema_input] ||= case options[:schema]
30+
when IO, StringIO then options[:schema]
31+
else RDF::Util::File.open_file(options[:schema]) {|f| f.read}
32+
end
33+
raise ArgumentError, "Shape matching requires a schema or reference to schema resource" unless options[:schema_input]
34+
raise ArgumentError, "Shape matching requires a focus node" unless options[:focus]
35+
format = options[:schema].to_s.end_with?('json') ? 'shexj' : 'shexc'
36+
shex = ShEx.parse(options[:schema_input], format: format, **options)
37+
38+
if options[:to_sxp] || options[:to_json]
39+
options[:messages][:shex] = {}
40+
options[:messages][:shex].merge!({"S-Expression": [SXP::Generator.string(shex.to_sxp_bin)]}) if options[:to_sxp]
41+
options[:messages][:shex].merge!({ShExJ: [shex.to_json(JSON::LD::JSON_STATE)]}) if options[:to_json]
42+
else
43+
focus = options.delete(:focus)
44+
shape = options.delete(:shape)
45+
map = shape ? {focus => shape} : {}
46+
begin
47+
res = shex.execute(RDF::CLI.repository, map, options.merge(focus: focus))
48+
options[:messages][:shex] = {
49+
result: ["Satisfied shape."],
50+
detail: [SXP::Generator.string(res.to_sxp_bin)]
51+
}
52+
rescue ShEx::NotSatisfied => e
53+
options[:logger].error e.to_s
54+
options[:messages][:shex] = {
55+
result: ["Did not satisfied shape."],
56+
detail: [SXP::Generator.stringe.expression]
57+
}
58+
raise
59+
end
60+
end
61+
end,
62+
options: [
63+
RDF::CLI::Option.new(
64+
symbol: :focus,
65+
datatype: String,
66+
control: :text,
67+
use: :required,
68+
on: ["--focus Resource"],
69+
description: "Focus node within repository"
70+
) {|v| RDF::URI(v)},
71+
RDF::CLI::Option.new(
72+
symbol: :shape,
73+
datatype: String,
74+
control: :text,
75+
use: :optional,
76+
on: ["--shape URI"],
77+
description: "Shape identifier within ShEx schema"
78+
) {|v| RDF::URI(v)},
79+
RDF::CLI::Option.new(
80+
symbol: :schema_input,
81+
datatype: String,
82+
control: :none,
83+
on: ["--schema-input STRING"],
84+
description: "ShEx schema in URI encoded format"
85+
) {|v| URI.decode(v)},
86+
RDF::CLI::Option.new(
87+
symbol: :schema,
88+
datatype: String,
89+
control: :url2,
90+
on: ["--schema URI"],
91+
description: "ShEx schema location"
92+
) {|v| RDF::URI(v)},
93+
RDF::CLI::Option.new(
94+
symbol: :to_json,
95+
datatype: String,
96+
control: :checkbox,
97+
on: ["--to-json"],
98+
description: "Display parsed schema as ShExJ"
99+
),
100+
RDF::CLI::Option.new(
101+
symbol: :to_sxp,
102+
datatype: String,
103+
control: :checkbox,
104+
on: ["--to-sxp"],
105+
description: "Display parsed schema as an S-Expression"
106+
),
107+
]
108+
}
109+
}
110+
end
111+
end
112+
end

spec/format_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# coding: utf-8
2+
$:.unshift "."
3+
require 'spec_helper'
4+
require 'rdf/spec/format'
5+
6+
describe ShEx::Format do
7+
it_behaves_like 'an RDF::Format' do
8+
let(:format_class) {ShEx::Format}
9+
end
10+
11+
describe ".for" do
12+
[
13+
:shex,
14+
"etc/doap.shex",
15+
{file_name: 'etc/doap.shex'},
16+
{file_extension: 'shex'},
17+
{content_type: 'application/shex'},
18+
].each do |arg|
19+
it "discovers with #{arg.inspect}" do
20+
expect(RDF::Format.for(arg)).to eq described_class
21+
end
22+
end
23+
end
24+
25+
describe "#to_sym" do
26+
specify {expect(described_class.to_sym).to eq :shex}
27+
end
28+
29+
describe ".cli_commands" do
30+
require 'rdf/cli'
31+
let(:ttl) {File.expand_path("../../etc/doap.ttl", __FILE__)}
32+
let(:schema) {File.expand_path("../../etc/doap.shex", __FILE__)}
33+
let(:schema_input) {File.read(schema)} # Not encoded, since decode done in option parsing
34+
let(:focus) {"http://rubygems.org/gems/shex"}
35+
let(:messages) {Hash.new}
36+
37+
describe "#shex" do
38+
it "matches from file" do
39+
expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema: schema, messages: messages)}.not_to write.to(:output)
40+
expect(messages).not_to be_empty
41+
end
42+
it "patches from StringIO" do
43+
expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema: StringIO.new(schema_input), messages: messages)}.not_to write.to(:output)
44+
expect(messages).not_to be_empty
45+
end
46+
it "patches from argument" do
47+
expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema_input: schema_input, messages: messages)}.not_to write.to(:output)
48+
expect(messages).not_to be_empty
49+
end
50+
end
51+
end
52+
end

spec/shexTest

Submodule shexTest updated 44 files

spec/shex_spec.rb

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
let(:doap_json) {File.expand_path("../../etc/doap.json", __FILE__)}
4545
let(:doap_ttl) {File.expand_path("../../etc/doap.ttl", __FILE__)}
4646
let(:doap_subj) {RDF::URI("http://rubygems.org/gems/shex")}
47-
let(:doap_shape) {RDF::URI("TestShape")}
47+
let(:doap_shape) {RDF::URI("http://rubygems.org/gems/shex/DOAP")}
4848
let(:doap_graph) {RDF::Graph.load(doap_ttl)}
4949
let(:doap_sxp) {%{(schema
50+
(base <http://rubygems.org/gems/shex/>)
5051
(prefix (("doap" <http://usefulinc.com/ns/doap#>) ("dc" <http://purl.org/dc/terms/>)))
52+
(start <http://rubygems.org/gems/shex/DOAP>)
5153
(shapes
5254
(shape
53-
(id <TestShape>)
55+
(id <http://rubygems.org/gems/shex/DOAP>)
5456
(extra a)
5557
(eachOf
5658
(tripleConstraint
@@ -59,30 +61,40 @@
5961
(oneOf
6062
(eachOf
6163
(tripleConstraint
62-
(predicate <http://usefulinc.com/ns/doap#name>)
63-
(nodeConstraint literal))
64+
(predicate <http://usefulinc.com/ns/doap#name>)
65+
(nodeConstraint literal))
6466
(tripleConstraint
6567
(predicate <http://usefulinc.com/ns/doap#description>)
6668
(nodeConstraint literal)) )
6769
(eachOf
68-
(tripleConstraint (predicate <http://purl.org/dc/terms/title>) (nodeConstraint literal))
69-
(tripleConstraint (predicate <http://purl.org/dc/terms/description>) (nodeConstraint literal)))
70-
(min 1) (max "*"))
71-
(tripleConstraint (predicate <http://usefulinc.com/ns/doap#category>)
70+
(tripleConstraint
71+
(predicate <http://purl.org/dc/terms/title>)
72+
(nodeConstraint literal))
73+
(tripleConstraint
74+
(predicate <http://purl.org/dc/terms/description>)
75+
(nodeConstraint literal)) )
76+
(min 1)
77+
(max "*"))
78+
(tripleConstraint
79+
(predicate <http://usefulinc.com/ns/doap#category>)
7280
(nodeConstraint iri)
73-
(min 0) (max "*"))
74-
(tripleConstraint (predicate <http://usefulinc.com/ns/doap#developer>)
81+
(min 0)
82+
(max "*"))
83+
(tripleConstraint
84+
(predicate <http://usefulinc.com/ns/doap#developer>)
7585
(nodeConstraint iri)
76-
(min 1) (max "*"))
77-
(tripleConstraint (predicate <http://usefulinc.com/ns/doap#implements>)
78-
(nodeConstraint (value <http://shex.io/shex-semantics/>))) ))))}.gsub(/^ /m, '')
86+
(min 1)
87+
(max "*"))
88+
(tripleConstraint
89+
(predicate <http://usefulinc.com/ns/doap#implements>)
90+
(nodeConstraint (value <http://shex.io/shex-semantics/>))) )) ))}.gsub(/^ /m, '')
7991
}
8092

8193
it "parses doap.shex" do
8294
expect(File.read(doap_shex)).to generate(doap_sxp)
8395
end
8496

85-
it "parses doap.json" do
97+
it "parses doap.json", skip: "base not in ShExJ" do
8698
sxp = doap_sxp.split("\n").reject {|l| l =~ /\(prefix/}.join("\n")
8799
expect(File.read(doap_json)).to generate(sxp, format: :shexj)
88100
end

0 commit comments

Comments
 (0)