-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcatalogue.rb
819 lines (696 loc) · 26.6 KB
/
catalogue.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
require 'intertwingler/vocab'
require 'intertwingler/handler'
require 'intertwingler/resource'
require 'intertwingler/document'
require 'intertwingler/representation/nokogiri'
require 'xml/mixup'
require 'rdf/rdfxml'
# This is a `GET` handler for what I'm calling "_catalogue_ resources"
# (sorry Americans). Its purpose is to tell us things like what's in
# the graph. Applications like the IBIS tool need a principled yet
# "dumb" way to access this kind of information in order to operate.
#
# The main goal of these catalogues is effectively to stand in for a
# handful of useful SPARQL queries (with inferencing) so a Web front
# end could construct a serviceable interactive user interface without
# having a lot of machinery written in JavaScript (much of which, like
# a _reasoner_, still does not exist at the time of this writing).
#
# Here are some specific questions the front end would want to ask the
# back end if it was setting up a user interface:
#
# * what classes are available to the application?
# * given a class (or classes), what properties have it (them) as a domain?
# * what properties have it (them) as a range?
# * given a property, what other resources in the graph…
# * …are directly in its domain?
# * …are in its domain by inference?
# * …are directly in its range?
# * …are in its range by inference?
# * …are already subjects (directly and by inference)?
# * …are already objects (directly and by inference)?
#
# The application's front end _should_ be able to answer these
# questions just by following links from wherever it currently is. It
# should not need to know any URLs in advance (except class and
# predicate identifiers, obviously, but it shouldn't need to know the
# URLs of any instance resources).
#
# Here is a sample chain of resources:
#
# ```turtle
# # how the actual entry point itself is done is negotiable
#
# ?wherever xhv:index ?idx .
#
# ?idx a cgto:Index ;
# cgto:class-summary ?sc ;
# cgto:property-summary ?sp .
#
# # (cgto:Summary rdfs:subClassOf qb:DataSet .)
#
# ?sc a cgto:Summary ;
# qb:structure cgto:resources-by-class .
#
# # cgto:resources-by-class and cgto:resources-by-property are both
# # instances of qb:DataStructureDefinition
#
# # observations have datasets, not the other way around, so:
#
# ?cobs a qb:Observation ;
# qb:dataSet ?sc ;
# cgto:class ?class ;
# cgto:asserted-instances ?ai ;
# cgto:asserted-instances-count ?aic ;
# cgto:inferred-instances ?ii ;
# cgto:inferred-instances-count ?iic .
#
# # the predicate one will look something like this
#
# ?sp a cgto:Summary
# qb:structure cgto:resources-by-property .
#
# # this is gonna be ugly cause there are 16 permutations of
# # asserted/inferred/subject/object/in-domain/in-range/resources/counts
#
# ?pobs a qb:Observation ;
# qb:dataSet ?sp ;
# cgto:property ?property ;
# cgto:asserted-subjects ?as ; # XXX
# cgto:asserted-subject-count ?asc ; # XXX
# cgto:inferred-subjects ?is ; # XXX
# cgto:inferred-subject-count ?isc ; # XXX
# cgto:asserted-objects ?ao ; # XXX
# cgto:asserted-object-count ?aoc ; # XXX
# cgto:inferred-objects ?io ; # XXX
# cgto:inferred-object-count ?ioc ; # XXX
# cgto:asserted-domain ?ad ;
# cgto:asserted-domain-count ?adc ;
# cgto:inferred-domain ?id ;
# cgto:inferred-domain-count ?idc ;
# cgto:asserted-range ?ar ;
# cgto:asserted-range-count ?arc ;
# cgto:inferred-range ?ir ;
# cgto:inferred-range-count ?irc .
#
# # ?as is asserted subjects; ?r1 is whatever resource being named
#
# ?as a cgto:Inventory ;
# dct:hasPart ?r1 . # , ...
# ```
#
# > In the IBIS tool I kind of unthinkingly added the `subjects`/
# > `objects` construct, but on further reflection I'm wondering how
# > useful it is. For starters, I never actually used it myself.
# > Second, it's redundant, at least from the point of view of any
# > discovery because it's not useful to know that this or that
# > resource is the subject or object of this or that predicate
# > outside of the context of the node of the other side of the
# > predicate (which, if you had access to the node, is information
# > you would already have), _except_ perhaps that aggregates will
# > tell you something about the usage of those particular predicates.
#
class Intertwingler::Handler::Catalogue < Intertwingler::Handler
private
CGTO = Intertwingler::Vocab::CGTO
QB = Intertwingler::Vocab::QB
XSD = RDF::XSD
XHV = RDF::Vocab::XHV
public
class Resource < Intertwingler::Resource
# XXX maybe make Intertwingler::Document a mixin? iunno
# all of these should have asserted/inferred variants; could even do
# a parameter for both so all four combinations (well, three of the
# four since asserted=false&inferred=false would give you nothing)
# are available.
private
def linkt target, **args
Intertwingler::Document.link_tag resolver, target, **args
end
def litt label, **args
Intertwingler::Document.literal_tag resolver, label, **args
end
def xhtml_response body, uri: nil, label: nil
uri ||= resolver.uri_for subject, slugs: true
label ||= repo.label_for(subject, noop: true).reverse
doc = XML::Mixup.xhtml_stub(
base: uri, title: label, content: body
).document
rep = Intertwingler::Representation::Nokogiri.new doc,
type: 'application/xhtml+xml'
Rack::Response[200, {
'content-type' => rep.type,
'content-length' => rep.size.to_s,
}, rep]
end
# this is more or less copied from perl
#
# @note `params[key]` is assumed to be a {::Range}
#
# @param params [Params::Registry::Instance] the parsed parameters
# @param key [Object] the key to pick out of the parameters
# @param items [Integer] the total number of items to paginate
# @param step [Integer] the default step size
#
# @return [Hash{Symbol=>Params::Registry::Instance}] the relevant
# parameter set
#
def pagination_params params, key, items, step: 100
min, max = params[key].minmax
min ||= 1
max ||= (min - 1) + step
rows = max - (min - 1)
# page = (min - 1) / rows # this is never used
last = items / rows + 1
ceil = last * rows
# the return value
out = {}
if min > 1
pmin = min - rows < 1 ? 1 : min - rows
pmax = max - rows < rows ? rows : max - rows
prev = out[:prev] = params.dup
prev[key] = [pmin, pmax]
if ceil > rows
x = out[:first] = params.dup
x[key] = [1, rows]
end
end
if min - 1 + rows < ceil
nmin = min + rows
nmax = (min - 1) + (rows * 2)
n = out[:next] = params.dup
n[key] = [nmin, nmax]
if ceil > rows
last = out[:last] = params.dup
last[key] = [(ceil - rows + 1), ceil]
end
end
out
end
# XXX CAN WE USE THIS IN GRAPHOPS PERHAPS?
VOCABS = [RDF::RDFV] + RDF::Vocabulary.to_a.drop(1)
def self.generate_stack properties: false
if properties
mth = :property?
eqv = :equivalentProperty
sup = :subPropertyOf
else
mth = :class?
eqv = :equivalentClass
sup = :subClassOf
end
VOCABS.reduce({}) do |hash, vocab|
vocab.each do |term|
# no blank nodes or other clutter
next unless term.uri? and term.respond_to? mth and term.send mth
# check if an equivalent class has already been entered into the hash
equivs = term.entail(eqv).to_set
record = hash.values_at(*equivs).compact.first || [Set[], Set[]]
if properties
record.push Set[], Set[] if record.length == 2
record[2].merge term.domain.select { |t| t.uri? }
record[3].merge term.range.select { |t| t.uri? }
end
# add ourselves
equivs << term
record[0].merge equivs
equivs.each do |e|
# get subclasses as no guarantee they were already got
record[1].merge e.send(sup).select(&:uri?) if e.respond_to? sup
# now link it up
hash[e] ||= record
end
end
hash
end
end
PREFIXES = RDF::Vocabulary.vocab_map.map do |prefix, struct|
[prefix, struct[:class] || RDF::Vocabulary.find(struct[:uri])]
end.to_h
# first we're gonna need to pull all the classes that we know about
CLASSES = generate_stack
PROPERTIES = generate_stack properties: true
DOMAINS = {}
RANGES = {}
# populate domains and ranges such that the keys are types
PROPERTIES.each do |prop, record|
{ 2 => DOMAINS, 3 => RANGES }.each do |index, mapping|
record[index].each { |type| (mapping[type] ||= Set[]) << prop }
end
end
PROPLIST = (%w[asserted inferred].product %w[domain range]).map do |p|
Intertwingler::Vocab::CGTO[p.join ?-]
end
def generic_set_for stack, term, include: false
out = Set[]
queue = [term]
while term = queue.shift
pair = stack[term] or next
# add the equivalents
out |= pair.first
# append supers to the queue before adding them to out
queue += (pair.last - out).to_a
# okay now add the supers
out |= pair.last
end
out -= [term] unless include
out
end
def class_set_for type, include: false
generic_set_for CLASSES, type, include: include
end
def property_set_for prop, include: false
generic_set_for PROPERTIES, prop, include: include
end
def finalize body
# XXX should we get this from the request??
# requri = resolver.uri_for subject, slugs: true
case body
when nil then nil
when Hash, Array then xhtml_response body
when Rack::Response then body
else nil
end
end
public
def get params: {}, headers: {}, body: nil
end
end
# Returns a meta-catalogue (`cgto:Index` that links to summaries).
# What I characterize as "almost static".
#
# @param base [URI] the user-facing subject URI
# @param args [Hash] throwaway keyword arguments for parity with
# other methods
#
# @return [Array] `<body>` contents to {XML::Mixup}
#
class Index < Resource
SUBJECT = RDF::URI('urn:uuid:f4792b48-92d8-4dcb-ae8a-c17199601cb9')
def get params: {}, headers: {}, body: nil
# uri = resolver.uri_for subject, slugs: true
# note the keys are method names not URL slugs
out = {
'4ab10425-d970-4280-8da2-7172822929ea' => CGTO['by-class'],
'611ed2d0-1544-4e0b-a4db-de942e1193e2' => CGTO['by-property'],
}.map do |uu, pred|
uri = resolver.uri_for uu, slugs: true, as: :uri
src = resolver.base.route_to uri
# we could hard-code these i suppose but this affords changing them
rel = resolver.abbreviate pred
type = resolver.abbreviate CGTO.Summary
# XXX do we want alternate representations of this??
{ { [''] => :script, type: 'application/xhtml+xml',
src: src, typeof: type } => :section, rel: rel }
end
finalize out
end
end
# all classes (cgto:Summary table)
#
# @param base [URI] the user-facing subject URI
# @param asserted [true, false] whether to include asserted types
# @param inferred [true, false] whether to include inferred types
#
# @return [Hash] an {XML::Mixup} representation of a table
#
class AllClasses < Resource
SUBJECT = RDF::URI('urn:uuid:4ab10425-d970-4280-8da2-7172822929ea')
def get params: {}, headers: {}, body: nil
# we also need what's going on in the inventory down there so we
# can present the counts
prefixes = PREFIXES.merge resolver.prefixes
# log.debug "fart lol"
# our product is something shaped like { type => [asserted, inferred] }
# counts = CLASSES.keys.map { |k| [k, [0, 0]] }.to_h
counts = {}
abbrs = {}
# then we scan the whole graph for ?s a ?t statements and
repo.query([nil, RDF.type, nil]).each do |stmt|
type = stmt.object
# only resources pls
next unless type.uri?
abbrs[type] ||= resolver.abbreviate(type, prefixes: prefixes)
# there may not be one already
(counts[type] ||= [0, 0])[0] += 1
# now do inferred
class_set_for(type).each do |inferred|
# doyy
abbrs[inferred] ||= resolver.abbreviate(inferred, prefixes: prefixes)
(counts[inferred] ||= [0, 0])[1] += 1
end
end
# log.debug abbrs.select {|_, v| v.nil? }.inspect
obst = resolver.abbreviate(QB.Observation)
# okay now we can sort it
obs = 0
body = counts.sort do |a, b|
# log.debug({ a.first => abbrs[a.first], b.first => abbrs[b.first]}).inspect
abbrs[a.first] <=> abbrs[b.first]
end.map do |type, record|
# XXX we have decided to make the sequence number into stable
# fragments so we can point back to them
row = "o.%d" % obs += 1
abt = "##{row}"
tt = resolver.abbreviate type.type, prefixes: prefixes if
type.respond_to? :type
asserted = litt(
RDF::Literal(record.first, datatype: XSD.nonNegativeInteger),
about: abt, property: CGTO['asserted-subject-count'])
inferred = litt(
RDF::Literal(record.last, datatype: XSD.nonNegativeInteger),
about: abt, property: CGTO['inferred-subject-count'])
st = resolver.abbreviate CGTO.Index
cols = [
{ linkt(type, typeof: tt, label: abbrs[type] || type) => :th },
{ linkt('', rel: CGTO['asserted-subjects'],
typeof: st, label: asserted) => :td },
{ linkt('', rel: CGTO['inferred-subjects'],
typeof: st, label: inferred) => :td },
]
{ cols => :tr, id: row, about: abt, typeof: obst }
end
finalize({ [
{ { [
{ ['Class'] => :th },
{ ['Asserted Subjects'] => :th },
{ ['Inferred Subjects'] => :th },
] => :tr } => :thead },
{ body => :tbody, rev: resolver.abbreviate(QB.dataSet) }
] => :table })
end
end
# all properties (cgto:Summary table)
#
# * all properties with domain T
# * all properties with range T
#
class AllProperties < Resource
SUBJECT = RDF::URI('urn:uuid:611ed2d0-1544-4e0b-a4db-de942e1193e2')
def get params: {}, headers: {}, body: nil
prefixes = PREFIXES.merge resolver.prefixes
# property => asserted (domain, range), inferred (domain, range)
counts = {} # our product { p => [ad, ar, id, ir] }
abbrs = {} # properties to CURIEs
repo.query([nil, RDF.type, nil]).each do |stmt|
type = stmt.object
next unless type.uri?
# XXX there is probably a cheaper way to do this, loopwise
# this one give us the initial asserted/inferred offsets
[Set[type], class_set_for(type)].each_with_index do |types, i|
# this index gives us the initial domain/range offsets
[DOMAINS, RANGES].each_with_index do |mapping, j|
# this will increment the correct slot
slot = i << 1 | j
types.each do |t|
mapping.fetch(t, []).each do |prop|
abbrs[prop] ||= resolver.abbreviate prop, prefixes: prefixes
(counts[prop] ||= [0, 0, 0, 0])[slot] += 1
end
end
end
end
end
body = counts.sort do |a, b|
abbrs[a.first] <=> abbrs[b.first]
end.map do |prop, record|
cols = [{ linkt(prop, label: abbrs[prop]) => :th }]
PROPLIST.each_with_index do |prop, i|
# we need the property and the count
cp = RDF::URI(prop.to_s + '-count')
cv = RDF::Literal(record[i], datatype: XSD.nonNegativeInteger)
cols << { linkt('', rel: prop, property: cp, label: cv) => :td }
end
{ cols => :tr }
end
finalize({ [
{ [
{ [
{ ['Property'] => :th, rowspan: 2 },
{ ['Asserted'] => :th, colspan: 2 },
{ ['Inferred'] => :th, colspan: 2 },
] => :tr },
{ [
{ ['In Domain'] => :th },
{ ['In Range'] => :th },
{ ['In Domain'] => :th },
{ ['In Range'] => :th }
] => :tr },
] => :thead },
{
body => :tbody,
rev: resolver.abbreviate(QB.dataSet, prefixes: prefixes)
} ] => :table })
end
end
# Return an inventory of resources found in the graph. Three
# parameters narrow the set returned:
#
# * `instance_of`: resources returned must be instances of these classes
# * `in_domain_of`: resources returned must be instances of classes
# in the _domain_ of these properties
# * `in_range_of`: resources returned must be instances of classes
# in the _range_ of these properties
#
# The set of classes produced by all three of these parameters is
# unioned together. If the `inferred` flag is set, both the
# properties and classes are expanded out to their equivalents and
# subproperties/classes. If all three parameters (`instance_of`,
# `in_domain_of`, `in_range_of`) are empty, this resource should
# just disgorge the entire set of subjects in the graph.
#
# If `inferred` is true, then the equivalents and
# subclasses/properties are tested as well. If `asserted` is false,
# then asserted classes, as well as classes directly asserted in the
# domains and ranges of properties, are _subtracted_ from the result
# set. If both `asserted` and `inferred` are false, this will raise
# a `409 Conflict`.
#
# The result set has the potential to be enormous, so we use the
# `boundary` parameter to paginate it.
#
# The document body returned by this function contains a single
# `<ol>` followed by a `<nav>` containing up to four pagination
# links (first, previous, next, last). It represents a
# `cgto:Inventory` and relates to its members via `dct:hasPart`.
# The sequence of members is not enforced in the data, but rather is
# derived from sorting {Intertwingler::GraphOps#label_for}.
#
# @param base [RDF::URI] the requested URI
# @param instance_of [RDF::URI, Array<RDF::URI>] a set of
# constraining RDF classes
# @param in_domain_of [RDF::URI, Array<RDF::URI>] a set of
# constraining properties by domain
# @param in_range_of [RDF::URI, Array<RDF::URI>] a set of
# constraining properties by range
# @param asserted [true, false] whether to include subjects whose
# directly asserted types match the constraints
# @param inferred [false, true] whether to include subjects whose
# types are inferred from equivalents or subclasses/properties
#
# @raise [Intertwingler::Handler::Redirect] when e.g. the parameters
# do not match when round-tripped or otherwise require adjustment
# @raise [Intertwingler::Handler::Conflict] when e.g. the parameters
# are wrong but only fixable by the user
#
# @return [Array] the body to pass into {XML::Mixup}
#
class Inventory < Resource
SUBJECT = RDF::URI('urn:uuid:bf4647be-7b02-4742-b482-567022a8c228')
def get params: {}, headers: {}, body: nil
# The job of this thing is to list individual resources, in a
# consistent order, with "best" label if applicable. Passing no
# (rather, default) parameters will yield a paginated list of
# all subjects in the graph. Results can be filtered first with
# an `instance-of` parameter which represents a set of classes,
# and `in-domain-of`/`in-range-of` parameters, which represent
# sets of properties. Each of these sets must be non-empty if it
# is to be included in the filtering, however the results
# returned will correspond to the union of all sets.
#
# Results can further be adjusted by using the boolean
# parameters `asserted` and `inferred`, although it is a 409
# Conflict error if both of these are false.
#
# ###
# step zero: bail with a conflict error if both asserted and
# inferred are false
raise Intertwingler::Handler::Error::Conflict,
'At least one of asserted or inferred parameters must be true' unless
params[:asserted] or params[:inferred]
# XXX step 0.5: resolve all the curies in the three sets in lieu
# of a functioning thing that will do this at the level of the parameter registry.
# instance-of OR (in-domain-of AND in-range-of)
terms = %i[instance-of in-domain-of in-range-of].reduce({}) do |h, k|
p = params[k] || Set[]
h[k] = [p.map { |t| resolver.resolve_curie t }.to_set, Set[]]
h
end
#
variants = [0]
variants << 1 if params[:inferred]
# step 1: map domain and range properties to asserted types
{ "in-domain-of": :domain, "in-range-of": :range}.each do |k, m|
# add to inferred
terms[k][1] |= repo.property_set terms[k][0] if params[:inferred]
variants.each do |v|
terms[:"instance-of"][v] |= terms[k][v].map do |t|
t.respond_to?(m) ? t.send(m) : nil
end.flatten.compact.select(&:iri?)
end
end
terms[:"instance-of"][1] |=
repo.type_strata(terms[:"instance-of"][0], descend: true) if
params[:inferred]
resources = Set[]
if terms.values.flatten.reduce(&:|).empty?
# just fetch everything
resources |= %i[subjects objects].map do |x|
repo.send(x).select &:iri?
end.reduce(&:+)
else
# there is something in at least one of these, so we filter
# let's get all the types that are actually in the graph
all = repo.all_types
variants.each do |v|
# do the properties
{ "in-domain-of": :subjects, "in-range-of": :objects }.each do |k, m|
terms[k][v].each do |p|
resources |= repo.query([nil, p, nil]).send(m).select(&:iri?)
end
end
#
(terms[:"instance-of"][v] & all).each do |t|
resources |= repo.query([nil, RDF.type, t]).subjects.select(&:iri?)
end
end
end
# warn params.inspect
# do the boundary
boundary = Range.new(*params[:boundary].minmax.map { |x| x - 1 })
# transform resources initially
resources = resources.map do |r|
x = { RDF.type => repo.types_for(r) }
lp, lo = repo.label_for r
x[lp] = [lo] if lp
[r, x]
end.to_h
# but we want to use it as a cache for this comparator
lcmp = repo.cmp_label cache: resources, nocase: true
li = resources.sort do |a, b|
lcmp.(a.first, b.first)
end.slice(boundary).map do |s, struct|
href = resolver.uri_for s, slugs: true
types = resolver.abbreviate(repo.types_for s, struct: struct)
lp, lo = repo.label_for s, struct: struct, noop: true
a = { '#a' => lo.value, href: href }
a[:typeof] = resolver.abbreviate types unless types.empty?
if lo.literal?
a[:property] = resolver.abbreviate lp
a['xml:lang'] = lo.language if lo.language?
a[:datatype] = resolver.abbreviate lo.datatype if lo.datatype?
end
{ "#li" => a }
end
uri = resolver.uri_for subject, as: :uri
pp = pagination_params params, :boundary, resources.size
nav = { first: 'First', prev: 'Previous',
next: 'Next', last: 'Last'}.map do |k, label|
if pp[k]
ku = pp[k].make_uri uri
linkt ku, rel: XHV[k], label: label
else
{ '#span' => label }
end
end
# XXX don't forget backlinks
finalize [{ li => :ol, start: boundary.begin + 1 }, { nav => :nav }]
end
end
class Me < Resource
SUBJECT = RDF::URI('urn:uuid:fe836b6d-11ef-48ef-9422-1747099b17ca')
def get params: {}, headers: {}, body: nil
end
end
class AllVocabs < Resource
SUBJECT = RDF::URI('urn:uuid:13e45ee1-0b98-4d4b-9e74-a83a09e85030')
def get params: {}, headers: {}, body: nil
io = StringIO.new '', 'w+', encoding: Encoding::BINARY
prefixes = resolver.prefixes.transform_values(&:to_uri)
RDF::Writer.for(:rdf).new(io, base_uri: base, prefixes: prefixes) do |writer|
resolver.prefixes.values.each do |vocab|
repo.query({ graph_name: vocab.to_uri }).each do |stmt|
# warn stmt.to_triple.inspect
writer << stmt.to_triple
end
end
end
# require 'pry'
# binding.pry
Rack::Response[200, {
'content-type' => 'application/rdf+xml',
'content-length' => io.size,
}, io]
end
end
private
MANIFEST = [Index, AllClasses, AllProperties,
Inventory, Me, AllVocabs].map { |c| [c.subject, c] }.to_h
# XXX we don't need this anymore
# ditto parameter names, which shooould come to us from upstream as
# compact uuids but we resolve them back to boring old symbols
PARAM_REV = {
instance_of: '9ebe1146-b658-4dfb-9ae2-8036883a96ac',
in_domain_of: '7170dcb2-aa31-4876-8817-dfe53ef79d69',
in_range_of: '3ef90f8b-a629-451e-94bf-da66c4a939bd',
asserted: 'fe4d51e5-db44-4ebf-8d7b-8f5b3edaedbb',
inferred: 'a6cf777a-2abf-46be-acc5-42625f335d03',
boundary: 'b348a477-61c6-4ab0-9a88-d9eda964f256',
}.transform_values { |v| RDF::URI("urn:uuid:#{v}".freeze) }
PARAM_MAP = PARAM_REV.invert
def get_headers req
req.env.select do |k|
%w[CONTENT_TYPE CONTENT_LENGTH].include? k or k.start_with? 'HTTP_'
end.transform_keys { |k| k.delete_prefix('HTTP_').downcase.tr(?_, ?-) }
end
public
def initialize engine, **args
# do whatever the superclass does
super engine, **args
# add the manifest
@manifest = MANIFEST.transform_values { |v| v.new self }
end
# General-purpose dispatch handler.
#
# @param req [Rack::Request] the request object
#
# @return [Rack::Response] a response
#
def handle req
# complain unless this is a GET or HEAD
# get the uri
uri = RDF::URI(req.url)
# orig = uri.dup
# clip off the query
query = uri.query || ''
uri.query = nil
# uuid in here??
subject = resolver.uuid_for uri, verify: false
resource = @manifest[subject] if subject
# get uuid or return 404
return Rack::Response[404, {
'content-type' => 'text/plain',
}, ['no catalogue']] unless subject and resource
# XXX this may raise an Intertwingler::Handler::AnyButSuccess
begin
resp = resource.call req.request_method, params: query,
headers: normalize_headers(req), body: req.body
rescue Intertwingler::Handler::AnyButSuccess => e
return e.response
end
return Rack::Response[404, {
'content-type' => 'text/plain',
}, ['no mapping']] unless resp
resp
end
end