rdf-kv 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/LICENSE +202 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rdf/kv.rb +421 -0
- data/lib/rdf/kv/version.rb +5 -0
- data/rdf-kv.gemspec +44 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: deb27dba84f5646d65c33ad83bd4ac842724ed0458f943194c572ef06b9ab679
|
4
|
+
data.tar.gz: 150094cd1b73702b87ada062070d66752bab1c28be502aaad8ca754fa7d51815
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 49fdd4acc90ca6089385473885b8fc49a9160e7d628eaf53242ab209f9b9858941d533df9966f7a77b853b20e4b203791ac43e58b566443ebab66e4196b18cfc
|
7
|
+
data.tar.gz: 58f9571d7c07fadbe58ca1e171382ea0a00ed43e86b02236dbba8e496be078a9ec62b29b03ce2d927b6ac609817318320adfa52d0f0049c62755f463ee20fe83
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
|
2
|
+
Apache License
|
3
|
+
Version 2.0, January 2004
|
4
|
+
http://www.apache.org/licenses/
|
5
|
+
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
7
|
+
|
8
|
+
1. Definitions.
|
9
|
+
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
12
|
+
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
14
|
+
the copyright owner that is granting the License.
|
15
|
+
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
17
|
+
other entities that control, are controlled by, or are under common
|
18
|
+
control with that entity. For the purposes of this definition,
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
20
|
+
direction or management of such entity, whether by contract or
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
23
|
+
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
25
|
+
exercising permissions granted by this License.
|
26
|
+
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
28
|
+
including but not limited to software source code, documentation
|
29
|
+
source, and configuration files.
|
30
|
+
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
32
|
+
transformation or translation of a Source form, including but
|
33
|
+
not limited to compiled object code, generated documentation,
|
34
|
+
and conversions to other media types.
|
35
|
+
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
37
|
+
Object form, made available under the License, as indicated by a
|
38
|
+
copyright notice that is included in or attached to the work
|
39
|
+
(an example is provided in the Appendix below).
|
40
|
+
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
47
|
+
the Work and Derivative Works thereof.
|
48
|
+
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
50
|
+
the original version of the Work and any modifications or additions
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
62
|
+
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
65
|
+
subsequently incorporated within the Work.
|
66
|
+
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
73
|
+
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
79
|
+
where such license applies only to those patent claims licensable
|
80
|
+
by such Contributor that are necessarily infringed by their
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
83
|
+
institute patent litigation against any entity (including a
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
86
|
+
or contributory patent infringement, then any patent licenses
|
87
|
+
granted to You under this License for that Work shall terminate
|
88
|
+
as of the date such litigation is filed.
|
89
|
+
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
92
|
+
modifications, and in Source or Object form, provided that You
|
93
|
+
meet the following conditions:
|
94
|
+
|
95
|
+
(a) You must give any other recipients of the Work or
|
96
|
+
Derivative Works a copy of this License; and
|
97
|
+
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
99
|
+
stating that You changed the files; and
|
100
|
+
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
103
|
+
attribution notices from the Source form of the Work,
|
104
|
+
excluding those notices that do not pertain to any part of
|
105
|
+
the Derivative Works; and
|
106
|
+
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
109
|
+
include a readable copy of the attribution notices contained
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
112
|
+
of the following places: within a NOTICE text file distributed
|
113
|
+
as part of the Derivative Works; within the Source form or
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
115
|
+
within a display generated by the Derivative Works, if and
|
116
|
+
wherever such third-party notices normally appear. The contents
|
117
|
+
of the NOTICE file are for informational purposes only and
|
118
|
+
do not modify the License. You may add Your own attribution
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
121
|
+
that such additional attribution notices cannot be construed
|
122
|
+
as modifying the License.
|
123
|
+
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
125
|
+
may provide additional or different license terms and conditions
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
129
|
+
the conditions stated in this License.
|
130
|
+
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
134
|
+
this License, without any additional terms or conditions.
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
136
|
+
the terms of any separate license agreement you may have executed
|
137
|
+
with Licensor regarding such Contributions.
|
138
|
+
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
141
|
+
except as required for reasonable and customary use in describing the
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
143
|
+
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
153
|
+
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
159
|
+
incidental, or consequential damages of any character arising as a
|
160
|
+
result of this License or out of the use or inability to use the
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
163
|
+
other commercial damages or losses), even if such Contributor
|
164
|
+
has been advised of the possibility of such damages.
|
165
|
+
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
169
|
+
or other liability obligations and/or rights consistent with this
|
170
|
+
License. However, in accepting such obligations, You may act only
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
175
|
+
of your accepting any such warranty or additional liability.
|
176
|
+
|
177
|
+
END OF TERMS AND CONDITIONS
|
178
|
+
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
180
|
+
|
181
|
+
To apply the Apache License to your work, attach the following
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
183
|
+
replaced with your own identifying information. (Don't include
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
185
|
+
comment syntax for the file format. We also recommend that a
|
186
|
+
file or class name and description of purpose be included on the
|
187
|
+
same "printed page" as the copyright notice for easier
|
188
|
+
identification within third-party archives.
|
189
|
+
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
191
|
+
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
193
|
+
you may not use this file except in compliance with the License.
|
194
|
+
You may obtain a copy of the License at
|
195
|
+
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
197
|
+
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
201
|
+
See the License for the specific language governing permissions and
|
202
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# RDF::KV — Turn Key-Value Pairs into an RDF::Changeset
|
2
|
+
|
3
|
+
This module is an implementation of the
|
4
|
+
[RDF-KV](https://doriantaylor.com/rdf-kv) protocol. This protocol
|
5
|
+
defines a method for embedding instructions for constructing a
|
6
|
+
[changeset](https://rubydoc.info/gems/rdf/RDF/Changeset) from ordinary
|
7
|
+
key-value pairs.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# initialize the processor
|
11
|
+
kv = RDF::KV.new subject: my_url, graph: graph_url
|
12
|
+
|
13
|
+
# use it to generate a changeset, e.g. from web form POST data
|
14
|
+
cs = kv.process rack.POST
|
15
|
+
|
16
|
+
# now apply it to your RDF::Repository
|
17
|
+
cs.apply repo
|
18
|
+
```
|
19
|
+
|
20
|
+
## Protocol Documentation
|
21
|
+
|
22
|
+
See [the very tentative spec](https://doriantaylor.com/rdf-kv).
|
23
|
+
|
24
|
+
## API Documentation
|
25
|
+
|
26
|
+
API documentation, for what it's worth at the moment, can be found [in
|
27
|
+
the usual place](https://rubydoc.info/github/doriantaylor/rb-rdf-kv/master).
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
You know how to do this:
|
32
|
+
|
33
|
+
$ gem install rdf-kv
|
34
|
+
|
35
|
+
Or, [download it off rubygems.org](https://rubygems.org/gems/rdf-kv).
|
36
|
+
|
37
|
+
## Contributing
|
38
|
+
|
39
|
+
Bug reports and pull requests are welcome at
|
40
|
+
[the GitHub repository](https://github.com/doriantaylor/rb-rdf-kv).
|
41
|
+
|
42
|
+
## Copyright & License
|
43
|
+
|
44
|
+
©2019 [Dorian Taylor](https://doriantaylor.com/)
|
45
|
+
|
46
|
+
This software is provided under
|
47
|
+
the [Apache License, 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rdf/kv"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/rdf/kv.rb
ADDED
@@ -0,0 +1,421 @@
|
|
1
|
+
require 'rdf/kv/version'
|
2
|
+
|
3
|
+
require 'rdf'
|
4
|
+
require 'uri'
|
5
|
+
require 'uuidtools'
|
6
|
+
require 'uuid-ncname'
|
7
|
+
|
8
|
+
class RDF::KV
|
9
|
+
private
|
10
|
+
|
11
|
+
# some xml grammar
|
12
|
+
NCNSCHAR = 'A-Za-z_\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u02ff' \
|
13
|
+
'\\u0370-\\u037d\\u037f-\\u1fff\\u200c-\\u200d\\u2070-\\u218f' \
|
14
|
+
'\\u2c00-\\u2fef\\u3001-\\ud7ff\\uf900-\\ufdcf\\ufdf0-\\ufffd' \
|
15
|
+
'\\u{10000}-\\u{effff}'.freeze
|
16
|
+
NSCHAR = "[:#{NCNSCHAR}]".freeze
|
17
|
+
NCNAMECHAR = "-.0-9#{NCNSCHAR}\\u00b7\\u0300-\\u036f\\u203f-\\u2040".freeze
|
18
|
+
NCNAME = "[#{NCNSCHAR}][#{NCNAMECHAR}]*".freeze
|
19
|
+
|
20
|
+
# the actual rdf-kv grammar
|
21
|
+
MODIFIER = '(?:[!=+-]|[+-]!|![+-])'.freeze
|
22
|
+
PREFIX = "(?:#{NCNAME}|[A-Za-z][0-9A-Za-z.+-]*)".freeze
|
23
|
+
TERM = "(?:#{PREFIX}:\\S*)".freeze
|
24
|
+
RFC5646 = '(?:[A-Za-z]+(?:-[0-9A-Za-z]+)*)'.freeze
|
25
|
+
DESIGNATOR = "(?:[:_']|@#{RFC5646}|\\^#{TERM})".freeze
|
26
|
+
DECLARATION = "^\\s*\\$\\s+(#{NCNAME})(?:\\s+(\\$))?\s*$".freeze
|
27
|
+
MACRO = "(?:\\$\\{(#{NCNAME})\\}|\\$(#{NCNAME}))".freeze
|
28
|
+
NOT_MACRO = "(?:(?!\\$#{NCNAME}|\\$\\{#{NCNAME}\\}).)*".freeze
|
29
|
+
MACROS = "(#{NOT_MACRO})(?:#{MACRO})?(#{NOT_MACRO})".freeze
|
30
|
+
PARTIAL_STMT = "^\\s*(?:(#{MODIFIER})\\s+)?" \
|
31
|
+
"(?:(#{TERM})(?:\\s+(#{TERM}))?(?:\\s+(#{DESIGNATOR}))?|" \
|
32
|
+
"(#{TERM})\\s+(#{DESIGNATOR})\\s+(#{TERM})|" \
|
33
|
+
"(#{TERM})\\s+(#{TERM})(?:\\s+(#{DESIGNATOR}))?\\s(#{TERM}))" \
|
34
|
+
"(?:\\s+(\\$))?\\s*$".freeze
|
35
|
+
|
36
|
+
GRAMMAR = /#{PARTIAL_STMT}/o
|
37
|
+
MAP = %i[modifier term1 term2 designator term1 designator graph
|
38
|
+
term1 term2 designator graph deref]
|
39
|
+
|
40
|
+
# these should be instance_exec'd
|
41
|
+
SPECIALS = {
|
42
|
+
SUBJECT: -> val {
|
43
|
+
@subject = resolve_term val[-1] unless val.empty? },
|
44
|
+
GRAPH: -> val {
|
45
|
+
@graph = resolve_term val[-1] unless val.empty? },
|
46
|
+
PREFIX: -> val {
|
47
|
+
val.each do |v, _|
|
48
|
+
next unless m = /^\s*(#{NCNAME}):\s+(.*)$/o.match(v)
|
49
|
+
prefix, uri = m.captures
|
50
|
+
@namespaces[prefix.to_sym] = RDF::Vocabulary.new uri
|
51
|
+
end
|
52
|
+
},
|
53
|
+
}
|
54
|
+
|
55
|
+
# macros are initially represented as a pair: the macro value and a
|
56
|
+
# flag denoting whether or not the macro itself contains macros and to
|
57
|
+
# try to dereference it.
|
58
|
+
GENERATED = {
|
59
|
+
NEW_UUID: [[-> { UUIDTools::UUID.random_create.to_s }, false]],
|
60
|
+
NEW_UUID_URN: [[-> { UUIDTools::UUID.random_create.to_uri }, false]],
|
61
|
+
NEW_BNODE: [[-> { "_:#{UUID::NCName.to_ncname_64(
|
62
|
+
UUIDTools::UUID.random_create.to_s, version: 1) }" }, false]],
|
63
|
+
}
|
64
|
+
|
65
|
+
# just the classics
|
66
|
+
DEFAULT_NS = {
|
67
|
+
rdf: RDF::RDFV,
|
68
|
+
rdfs: RDF::RDFS,
|
69
|
+
owl: RDF::OWL,
|
70
|
+
xsd: RDF::XSD,
|
71
|
+
}.freeze
|
72
|
+
|
73
|
+
def deref_content strings, macros
|
74
|
+
strings = [strings] unless strings.is_a? Array
|
75
|
+
# bail out early if there is nothing to do
|
76
|
+
return strings unless strings.any? { |s| /#{MACRO}/o.match s }
|
77
|
+
out = []
|
78
|
+
strings.each do |s|
|
79
|
+
# sometimes these are arrays of arrays
|
80
|
+
#s = s.first if s.is_a? Array
|
81
|
+
|
82
|
+
# chunks are parallel output; each element is a value
|
83
|
+
chunks = []
|
84
|
+
s.scan(/\G#{MACROS}/o) do |m|
|
85
|
+
pre = m.first
|
86
|
+
macro = m[1] || m[2]
|
87
|
+
post = m[3]
|
88
|
+
|
89
|
+
# skip if there was no macro
|
90
|
+
unless macro
|
91
|
+
# nothing to do
|
92
|
+
next if pre + post == ""
|
93
|
+
chunks = chunks.empty? ? [pre, post] : chunks.map do |x|
|
94
|
+
"#{x}#{pre}#{post}"
|
95
|
+
end
|
96
|
+
next
|
97
|
+
end
|
98
|
+
|
99
|
+
# dereference the macro (or noop if unbound)
|
100
|
+
macro = macro.to_sym
|
101
|
+
x = if macros[macro]
|
102
|
+
macros[macro].map do |m|
|
103
|
+
'%s%s%s' % [pre, m.respond_to?(:call) ? m.call : m, post]
|
104
|
+
end
|
105
|
+
else
|
106
|
+
# this is a noop
|
107
|
+
["#{pre}$#{macro}#{post}"]
|
108
|
+
end
|
109
|
+
|
110
|
+
# initialize chunks
|
111
|
+
if chunks.empty?
|
112
|
+
chunks = x
|
113
|
+
next
|
114
|
+
elsif !x.empty?
|
115
|
+
# replace chunks with the product of itself and x
|
116
|
+
y = []
|
117
|
+
chunks.each { |c| x.each { |d| y << "#{c}#{d}" } }
|
118
|
+
chunks = y
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
out.concat chunks
|
123
|
+
end
|
124
|
+
|
125
|
+
out
|
126
|
+
end
|
127
|
+
|
128
|
+
def massage_macros macros
|
129
|
+
seen = {}
|
130
|
+
done = GENERATED.transform_values { |v| v.map { |w| w.first } }
|
131
|
+
pending = macros.reject { |k, _| GENERATED.key? k }
|
132
|
+
queue = pending.keys.slice 0..0 # take a zero-or-one-element slice
|
133
|
+
|
134
|
+
until queue.empty?
|
135
|
+
k = queue.shift
|
136
|
+
seen[k] = true
|
137
|
+
|
138
|
+
vals = macros[k]
|
139
|
+
|
140
|
+
# done and pending macros within the macros
|
141
|
+
dm = {}
|
142
|
+
pm = {}
|
143
|
+
|
144
|
+
vals.each do |pair|
|
145
|
+
val, deref = pair
|
146
|
+
|
147
|
+
next unless deref
|
148
|
+
|
149
|
+
if deref.is_a? Array
|
150
|
+
deref.each do |m|
|
151
|
+
done[m] ? dm[m] = true : pm[m] = true
|
152
|
+
end
|
153
|
+
else
|
154
|
+
m = {}
|
155
|
+
val.scan(/#{MACRO}/o).compact.each do |x|
|
156
|
+
x = x.to_sym
|
157
|
+
next unless macros[x]
|
158
|
+
raise "Self-reference found: #{x}" if x == k
|
159
|
+
|
160
|
+
m[x] = true
|
161
|
+
|
162
|
+
done[m] ? dm[m] = true : pm[m] = true
|
163
|
+
end
|
164
|
+
# push the deref
|
165
|
+
pair[1] = m.empty? ? false : m.keys.sort
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# macro values have pending matches
|
170
|
+
if !pm.empty?
|
171
|
+
q = []
|
172
|
+
pm.keys.each do |m|
|
173
|
+
raise "Cycle detected between #{k} and #{m}" if seen[m]
|
174
|
+
q << m
|
175
|
+
end
|
176
|
+
|
177
|
+
queue = q + [k] + queue
|
178
|
+
next
|
179
|
+
end
|
180
|
+
|
181
|
+
unless dm.empty?
|
182
|
+
done[k] = deref_content vals, done
|
183
|
+
else
|
184
|
+
done[k] = vals.map(&:first)
|
185
|
+
end
|
186
|
+
|
187
|
+
# remember to remove this guy or we'll loop forever
|
188
|
+
pending.delete k
|
189
|
+
|
190
|
+
# replenish the queue with another pending object
|
191
|
+
queue << pending.keys.first if queue.empty? and !pending.keys.empty?
|
192
|
+
end
|
193
|
+
|
194
|
+
done
|
195
|
+
end
|
196
|
+
|
197
|
+
# unconditionally return a uri or bnode
|
198
|
+
def resolve_term term
|
199
|
+
return term if term.is_a? RDF::Term
|
200
|
+
term = term.to_s
|
201
|
+
|
202
|
+
# bnode ahoy
|
203
|
+
return RDF::Node.new term.delete_prefix '_:' if term.start_with? '_:'
|
204
|
+
|
205
|
+
# ugh now we gotta do urls
|
206
|
+
if m = /^(#{NCNAME}):(\S*)$/o.match(term)
|
207
|
+
prefix, slug = m.captures
|
208
|
+
if !slug.start_with?(?/) and vocab = namespaces[prefix.to_sym]
|
209
|
+
return vocab[slug]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# now resolve against base
|
214
|
+
RDF::URI((URI(subject.to_s) + term).to_s)
|
215
|
+
end
|
216
|
+
|
217
|
+
# may accept and respond with nil
|
218
|
+
def coerce_term token, hint = nil, langdt = nil
|
219
|
+
return unless token
|
220
|
+
return token if token.is_a? RDF::Term
|
221
|
+
hint ||= ?:
|
222
|
+
term = nil
|
223
|
+
if [?:, ?_].include? hint
|
224
|
+
return if token.empty?
|
225
|
+
token = '_:' + token if hint == ?_ and !token.start_with? '_:'
|
226
|
+
term = resolve_term token
|
227
|
+
elsif hint == ?@
|
228
|
+
term = RDF::Literal(token, language: langdt.to_s.to_sym)
|
229
|
+
elsif hint == ?^
|
230
|
+
raise 'datatype must be an RDF::Resource' unless
|
231
|
+
langdt.is_a? RDF::Resource
|
232
|
+
term = RDF::Literal(token, datatype: langdt)
|
233
|
+
elsif hint == ?'
|
234
|
+
term = RDF::Literal(token)
|
235
|
+
else
|
236
|
+
raise ArgumentError, "Unrecognized hint (#{hint})"
|
237
|
+
end
|
238
|
+
|
239
|
+
# call the callback if we have one
|
240
|
+
term = callback.call term if callback
|
241
|
+
|
242
|
+
term
|
243
|
+
end
|
244
|
+
|
245
|
+
public
|
246
|
+
|
247
|
+
attr_reader :subject, :graph, :namespaces, :callback
|
248
|
+
|
249
|
+
# Initialize the processor.
|
250
|
+
#
|
251
|
+
# @param subject [RDF::URI] The default subject. Required.
|
252
|
+
# @param graph [RDF::URI] The default context. Optional.
|
253
|
+
# @param namespaces [Hash] Namespace/prefix mappings. Optional.
|
254
|
+
# @param callback [#call] A callback that expects and returns a term.
|
255
|
+
# Optional.
|
256
|
+
#
|
257
|
+
def initialize subject: nil, graph: nil, namespaces: {}, callback: nil
|
258
|
+
# look at all of our pretty assertions
|
259
|
+
raise ArgumentError, 'subject must be an RDF::Resource' unless
|
260
|
+
subject.is_a? RDF::Resource
|
261
|
+
raise ArgumentError, 'graph must be an RDF::Resource' unless
|
262
|
+
graph.nil? or graph.is_a? RDF::Resource
|
263
|
+
raise ArgumentError, 'namespaces must be hashable' unless
|
264
|
+
namespaces.respond_to? :to_h
|
265
|
+
rase ArgumentError, 'callback must be callable' unless
|
266
|
+
callback.nil? or callback.respond_to? :call
|
267
|
+
|
268
|
+
@subject = subject
|
269
|
+
@graph = graph
|
270
|
+
@callback = callback
|
271
|
+
@namespaces = DEFAULT_NS.merge(namespaces.to_h.map do |k, v|
|
272
|
+
k = k.to_s.to_sym unless k.is_a? Symbol
|
273
|
+
# coerce to uri
|
274
|
+
v = RDF::URI(v.to_s) unless v.is_a? RDF::Resource
|
275
|
+
# now coerce to vocabulary
|
276
|
+
v = RDF::Vocabulary.new v unless v.is_a? RDF::Vocabulary
|
277
|
+
[k, v]
|
278
|
+
end.to_h)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Process a hash of form input.
|
282
|
+
|
283
|
+
# @note This operation may change the state of the processor, so
|
284
|
+
# while this object can be reused for multiple hashes, it is unwise
|
285
|
+
# to reuse it across requests.
|
286
|
+
#
|
287
|
+
# @param data [Hash] The data coming, e.g., from the Web form.
|
288
|
+
# @return [RDF::Changeset] A changeset containing the results.
|
289
|
+
#
|
290
|
+
def process data
|
291
|
+
raise ArgumentError, 'data must be a hash' unless data.is_a? Hash
|
292
|
+
macros = GENERATED.dup
|
293
|
+
maybe = {} # candidates
|
294
|
+
neither = {} # discard pile
|
295
|
+
|
296
|
+
data.each do |k, *v|
|
297
|
+
# step 0: get the values to a homogeneous list
|
298
|
+
k = k.to_s
|
299
|
+
v = v.flatten.map(&:to_s)
|
300
|
+
# step 1: pull out all the macro declarations
|
301
|
+
if (m = /#{DECLARATION}/o.match k)
|
302
|
+
name = m[1].to_sym
|
303
|
+
sigil = !!(m[2] && !m[2].empty?)
|
304
|
+
# skip over generated macros
|
305
|
+
next if GENERATED.key? name
|
306
|
+
# step 1.0.1: create [content, deref flag] pairs
|
307
|
+
(macros[name] ||= []).concat v.map { |x| [x, sigil] }
|
308
|
+
elsif (m = /(?:^\s*\S+\s+\S+.*?$|[:\$])/.match k)
|
309
|
+
(maybe[k] ||= []).concat v
|
310
|
+
else
|
311
|
+
(neither[k] ||= []).concat v
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# step 2: dereference all the macros (that asked to be dereferenced)
|
316
|
+
begin
|
317
|
+
macros = massage_macros macros
|
318
|
+
rescue e
|
319
|
+
# XXX we should do something more here
|
320
|
+
raise e
|
321
|
+
end
|
322
|
+
|
323
|
+
# step 3: apply special control macros (which modify self)
|
324
|
+
begin
|
325
|
+
SPECIALS.each do |k, macro|
|
326
|
+
instance_exec macros[k], ¯o if macros[k]
|
327
|
+
end
|
328
|
+
rescue Exception => e
|
329
|
+
# again this should be nicer
|
330
|
+
raise e
|
331
|
+
end
|
332
|
+
|
333
|
+
# this will be our output
|
334
|
+
patch = RDF::Changeset.new
|
335
|
+
|
336
|
+
maybe.each do |k, v|
|
337
|
+
# this will return an array now
|
338
|
+
k = deref_content(k, macros).compact
|
339
|
+
v = v.compact.map(&:strip).uniq
|
340
|
+
|
341
|
+
# this is only this way because of macros
|
342
|
+
k.each do |template|
|
343
|
+
tokens = GRAMMAR.match(template) or next
|
344
|
+
tokens = tokens.captures
|
345
|
+
|
346
|
+
raise 'INTERNAL ERROR: Regexp captures do not match template' unless
|
347
|
+
tokens.length == MAP.length
|
348
|
+
|
349
|
+
# i had something much cleverer here but of course it didn't DWIW
|
350
|
+
contents = {}
|
351
|
+
MAP.each_index { |i| contents[MAP[i]] ||= tokens[i] }
|
352
|
+
contents.compact!
|
353
|
+
|
354
|
+
contents[:modifier] = (contents[:modifier] || '').chars.map do |c|
|
355
|
+
[c, true]
|
356
|
+
end.to_h
|
357
|
+
|
358
|
+
if contents[:designator]
|
359
|
+
sigil, symbol = contents[:designator].split '', 2
|
360
|
+
symbol = resolve_term symbol if sigil == ?^
|
361
|
+
contents[:designator] = symbol.to_s.empty? ? [sigil] : [sigil, symbol]
|
362
|
+
else
|
363
|
+
contents[:designator] = [contents[:modifier][?!] ? ?: : ?']
|
364
|
+
end
|
365
|
+
|
366
|
+
%i[term1 term2 graph].filter { |t| contents[t] }.each do |which|
|
367
|
+
contents[which] = resolve_term contents[which]
|
368
|
+
end
|
369
|
+
|
370
|
+
# these are the values we actually use; ensure they are duplicated
|
371
|
+
values = (contents[:deref] ? deref_content(v, macros) : v).dup
|
372
|
+
|
373
|
+
g = coerce_term(contents[:graph]) || graph
|
374
|
+
# initialize the triple
|
375
|
+
s, p, o = nil
|
376
|
+
|
377
|
+
# shorthand for reverse
|
378
|
+
if reverse = !!contents[:modifier][?!]
|
379
|
+
# literals make no sense on reverse statements
|
380
|
+
# (XXX this is a candidate for diagnostics)
|
381
|
+
next unless [?_, ?:].include? contents[:designator].first
|
382
|
+
# these terms have already been resolved/coerced
|
383
|
+
p = contents[:term1]
|
384
|
+
o = contents[:term2] || subject
|
385
|
+
else
|
386
|
+
s, p = (contents[:term2] ? contents.values_at(:term1, :term2) :
|
387
|
+
[subject, contents[:term1]]).map { |t| resolve_term t }
|
388
|
+
end
|
389
|
+
|
390
|
+
# the operation depends on whether the `-` modifier is present
|
391
|
+
op = contents[:modifier][?-] ? :delete : :insert
|
392
|
+
|
393
|
+
# if we're deleting triples and the values contain an empty
|
394
|
+
# string then we're deleting a wildcard, same if we `=` overwrite
|
395
|
+
if !reverse and op == :delete && values.include?('') ||
|
396
|
+
contents[:modifier][?=]
|
397
|
+
# i can't remember why we don't do this in reverse, probably
|
398
|
+
# because it is too easy to shoot yourself in the foot
|
399
|
+
patch.delete RDF::Statement(s, p, nil, graph_name: g)
|
400
|
+
|
401
|
+
# nuke these since it will be pointless to evaluate further
|
402
|
+
values.clear if op == :delete
|
403
|
+
end
|
404
|
+
|
405
|
+
# otherwise the code is basically the same
|
406
|
+
values.each do |x|
|
407
|
+
# get what should be guaranteed to be an RDF term or nil
|
408
|
+
x = coerce_term(x, *contents[:designator]) or next
|
409
|
+
|
410
|
+
# now we assign the appropriate direction
|
411
|
+
reverse ? s = x : o = x
|
412
|
+
|
413
|
+
# this will be either insert or delete
|
414
|
+
patch.send op, RDF::Statement(s, p, o, graph_name: g)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
patch
|
420
|
+
end
|
421
|
+
end
|
data/rdf-kv.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- mode: enh-ruby -*-
|
2
|
+
require_relative 'lib/rdf/kv/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = 'rdf-kv'
|
6
|
+
spec.version = RDF::KV::VERSION
|
7
|
+
spec.authors = ['Dorian Taylor']
|
8
|
+
spec.email = ['code@doriantaylor.com']
|
9
|
+
spec.license = 'Apache-2.0'
|
10
|
+
spec.homepage = 'https://github.com/doriantaylor/rb-rdf-kv'
|
11
|
+
spec.summary = 'Ruby implementation of the RDF-KV protocol'
|
12
|
+
spec.description = <<-DESC
|
13
|
+
This module implements https://doriantaylor.com/rdf-kv, taking
|
14
|
+
key-value input (e.g. from a Web form) and converting it into an
|
15
|
+
RDF::Changeset.
|
16
|
+
DESC
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is
|
21
|
+
# released. The `git ls-files -z` loads the files in the RubyGem
|
22
|
+
# that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
f.match(%r{^(test|spec|features)/})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
33
|
+
# dev/test dependencies
|
34
|
+
spec.add_development_dependency 'bundler', '~> 2'
|
35
|
+
spec.add_development_dependency 'rdf-vocab', '~> 3.1'
|
36
|
+
|
37
|
+
# stuff we use
|
38
|
+
spec.add_runtime_dependency 'rdf', '>= 3.1.1' # include my changes
|
39
|
+
spec.add_runtime_dependency 'uuidtools', '~> 2'
|
40
|
+
|
41
|
+
# stuff i wrote
|
42
|
+
spec.add_runtime_dependency 'uuid-ncname', '>= 0.2.5'
|
43
|
+
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rdf-kv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dorian Taylor
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-01-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rdf-vocab
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rdf
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.1.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: uuidtools
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: uuid-ncname
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.2.5
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.2.5
|
83
|
+
description: |
|
84
|
+
This module implements https://doriantaylor.com/rdf-kv, taking
|
85
|
+
key-value input (e.g. from a Web form) and converting it into an
|
86
|
+
RDF::Changeset.
|
87
|
+
email:
|
88
|
+
- code@doriantaylor.com
|
89
|
+
executables: []
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- ".gitignore"
|
94
|
+
- ".rspec"
|
95
|
+
- ".travis.yml"
|
96
|
+
- Gemfile
|
97
|
+
- LICENSE
|
98
|
+
- README.md
|
99
|
+
- Rakefile
|
100
|
+
- bin/console
|
101
|
+
- bin/setup
|
102
|
+
- lib/rdf/kv.rb
|
103
|
+
- lib/rdf/kv/version.rb
|
104
|
+
- rdf-kv.gemspec
|
105
|
+
homepage: https://github.com/doriantaylor/rb-rdf-kv
|
106
|
+
licenses:
|
107
|
+
- Apache-2.0
|
108
|
+
metadata:
|
109
|
+
homepage_uri: https://github.com/doriantaylor/rb-rdf-kv
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 2.3.0
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubygems_version: 3.1.2
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: Ruby implementation of the RDF-KV protocol
|
129
|
+
test_files: []
|