• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 28937277207

08 Jul 2026 10:55AM UTC coverage: 28.314% (+0.2%) from 28.115%
28937277207

push

github

web-flow
Merge pull request #545 from knowledgepixels/feat/template-embedded-identity

feat: identify templates by embedded IRIs with dual-mode parser

1838 of 7333 branches covered (25.06%)

Branch coverage included in aggregate %.

3749 of 12399 relevant lines covered (30.24%)

4.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.28
src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java
1
package com.knowledgepixels.nanodash.template;
2

3
import com.knowledgepixels.nanodash.LocalUri;
4
import com.knowledgepixels.nanodash.NanodashSession;
5
import com.knowledgepixels.nanodash.Utils;
6
import com.knowledgepixels.nanodash.component.LiteralDateItem;
7
import com.knowledgepixels.nanodash.component.PublishForm.FillMode;
8
import com.knowledgepixels.nanodash.component.StatementItem;
9
import org.apache.wicket.Component;
10
import org.apache.wicket.model.IModel;
11
import org.apache.wicket.model.Model;
12
import org.eclipse.rdf4j.model.IRI;
13
import org.eclipse.rdf4j.model.Statement;
14
import org.eclipse.rdf4j.model.Value;
15
import org.eclipse.rdf4j.model.ValueFactory;
16
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
17
import org.eclipse.rdf4j.model.vocabulary.RDFS;
18
import org.eclipse.rdf4j.model.vocabulary.XSD;
19
import org.nanopub.*;
20
import org.nanopub.vocabulary.NTEMPLATE;
21

22
import java.io.Serializable;
23
import java.time.ZonedDateTime;
24
import java.util.*;
25

26
/**
27
 * Context for a template, containing all necessary information to fill.
28
 */
29
public class TemplateContext implements Serializable {
30

31
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TemplateContext.class);
9✔
32
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
9✔
33

34
    private final ContextType contextType;
35
    private final Template template;
36
    private final String componentId;
37
    private final Map<String, String> params = new HashMap<>();
15✔
38
    private List<Component> components = new ArrayList<>();
15✔
39
    private final Map<IRI, IModel<?>> componentModels = new HashMap<>();
15✔
40
    private Set<IRI> introducedIris = new HashSet<>();
15✔
41
    private Set<IRI> embeddedIris = new HashSet<>();
15✔
42
    private Map<IRI, IRI> rolePropertyPins = new LinkedHashMap<>();
15✔
43
    private List<StatementItem> statementItems;
44
    private Set<IRI> iriSet = new HashSet<>();
15✔
45
    private Map<IRI, StatementItem> narrowScopeMap = new HashMap<>();
15✔
46
    private String targetNamespace = Template.DEFAULT_TARGET_NAMESPACE;
9✔
47
    private Nanopub existingNanopub;
48
    private Nanopub fillSource;
49
    private Map<IRI, String> labels;
50
    private FillMode fillMode = null;
9✔
51

52
    /**
53
     * Constructor for creating a new template context for filling a template.
54
     *
55
     * @param contextType     the type of context
56
     * @param templateId      the ID of the template to fill
57
     * @param componentId     the ID of the component that will use this context
58
     * @param targetNamespace the target namespace for the template, can be null to use the default namespace
59
     */
60
    public TemplateContext(ContextType contextType, String templateId, String componentId, String targetNamespace) {
61
        this(contextType, templateId, componentId, targetNamespace, null);
21✔
62
    }
3✔
63

64
    /**
65
     * Constructor for creating a new template context for filling a template.
66
     *
67
     * @param contextType     the type of context
68
     * @param templateId      the ID of the template to fill
69
     * @param componentId     the ID of the component that will use this context
70
     * @param existingNanopub an existing nanopub to fill, can be null if creating a new nanopub
71
     */
72
    public TemplateContext(ContextType contextType, String templateId, String componentId, Nanopub existingNanopub) {
73
        this(contextType, templateId, componentId, null, existingNanopub);
21✔
74
    }
3✔
75

76
    private TemplateContext(ContextType contextType, String templateId, String componentId, String targetNamespace, Nanopub existingNanopub) {
6✔
77
        this.contextType = contextType;
9✔
78
        // TODO: check whether template is of correct type:
79
        this.template = TemplateData.get().getTemplate(templateId);
15✔
80
        this.componentId = componentId;
9✔
81
        if (targetNamespace != null) {
6✔
82
            this.targetNamespace = targetNamespace;
9✔
83
        }
84
        this.existingNanopub = existingNanopub;
9✔
85
        if (existingNanopub == null && NanodashSession.get().getUserIri() != null) {
15!
86
            componentModels.put(NTEMPLATE.CREATOR_PLACEHOLDER, Model.of(NanodashSession.get().getUserIri().stringValue()));
×
87
        }
88
    }
3✔
89

90
    /**
91
     * Initializes the statements for this context.
92
     */
93
    public void initStatements() {
94
        if (statementItems != null) return;
9!
95
        statementItems = new ArrayList<>();
15✔
96
        for (IRI st : template.getStatementIris()) {
36✔
97
            StatementItem si = new StatementItem(componentId, st, this);
24✔
98
            statementItems.add(si);
15✔
99
            for (IRI i : si.getIriSet()) {
33✔
100
                if (iriSet.contains(i)) {
15✔
101
                    narrowScopeMap.remove(i);
18✔
102
                } else {
103
                    iriSet.add(i);
15✔
104
                    narrowScopeMap.put(i, si);
18✔
105
                }
106
            }
3✔
107
        }
3✔
108
    }
3✔
109

110
    /**
111
     * Finalizes the statements by processing all parameters and setting the repetition counts.
112
     */
113
    public void finalizeStatements() {
114
        Map<StatementItem, Integer> finalRepetitionCount = new HashMap<>();
12✔
115
        for (IRI ni : narrowScopeMap.keySet()) {
36✔
116
            // TODO: Move all occurrences of this to utility function:
117
            String postfix = Utils.getUriPostfix(ni);
9✔
118
            StatementItem si = narrowScopeMap.get(ni);
18✔
119
            int i = si.getRepetitionCount();
9✔
120
            while (true) {
121
                String p = postfix + "__" + i;
12✔
122
                if (hasParam(p)) {
12!
123
                    si.addRepetitionGroup();
×
124
                } else {
125
                    break;
126
                }
127
                i++;
×
128
            }
×
129
            i = 1;
6✔
130
            int corr = 0;
6✔
131
            if (si.isEmpty()) corr = 1;
15✔
132
            while (true) {
133
                String p = postfix + "__." + i;
12✔
134
                if (hasParam(p)) {
12✔
135
                    int absPos = si.getRepetitionCount() + i - 1 - corr;
27✔
136
                    String param = postfix + "__" + absPos;
12✔
137
                    if (i - corr == 0) param = postfix;
12!
138
                    setParam(param, getParam(p));
18✔
139
                    finalRepetitionCount.put(si, i - corr);
24✔
140
                } else {
141
                    break;
142
                }
143
                i++;
3✔
144
            }
3✔
145
        }
3✔
146
        for (StatementItem si : finalRepetitionCount.keySet()) {
33✔
147
            for (int i = 0; i < finalRepetitionCount.get(si); i++) {
33✔
148
                si.addRepetitionGroup();
6✔
149
            }
150
        }
3✔
151
        for (StatementItem si : statementItems) {
33✔
152
            si.finalizeValues();
6✔
153
        }
3✔
154
    }
3✔
155

156
    /**
157
     * Sets the fill mode for this context.
158
     *
159
     * @param fillMode the fill mode to set
160
     */
161
    public void setFillMode(FillMode fillMode) {
162
        this.fillMode = fillMode;
9✔
163
    }
3✔
164

165
    /**
166
     * Gets the fill mode for this context.
167
     *
168
     * @return the fill mode, or null if not set
169
     */
170
    public FillMode getFillMode() {
171
        return fillMode;
9✔
172
    }
173

174
    /**
175
     * Returns the type of context.
176
     *
177
     * @return the context type
178
     */
179
    public ContextType getType() {
180
        return contextType;
9✔
181
    }
182

183
    /**
184
     * Returns the template associated with this context.
185
     *
186
     * @return the template
187
     */
188
    public Template getTemplate() {
189
        return template;
9✔
190
    }
191

192
    /**
193
     * Returns the ID of the template associated with this context.
194
     *
195
     * @return the template ID
196
     */
197
    public String getTemplateId() {
198
        return template.getId();
×
199
    }
200

201
    /**
202
     * Returns the URI of the nanopublication containing the template of this
203
     * context. Placeholder and other local IRIs of a template are minted under
204
     * this URI, so use it (not {@link #getTemplateId()}, which may be an embedded
205
     * sub-IRI) wherever a local-IRI prefix is stripped or constructed.
206
     *
207
     * @return the template's nanopublication URI
208
     */
209
    public String getTemplateNanopubUri() {
210
        return template.getNanopub().getUri().stringValue();
18✔
211
    }
212

213
    /**
214
     * Sets a parameter for this context.
215
     *
216
     * @param name  the name of the parameter
217
     * @param value the value of the parameter
218
     */
219
    public void setParam(String name, String value) {
220
        params.put(name, value);
18✔
221
    }
3✔
222

223
    /**
224
     * Gets a parameter value by its name.
225
     *
226
     * @param name the name of the parameter
227
     * @return the value of the parameter, or null if not set
228
     */
229
    public String getParam(String name) {
230
        return params.get(name);
18✔
231
    }
232

233
    /**
234
     * Checks if a parameter with the given name exists.
235
     *
236
     * @param name the name of the parameter
237
     * @return true if the parameter exists, false otherwise
238
     */
239
    public boolean hasParam(String name) {
240
        return params.containsKey(name);
15✔
241
    }
242

243
    /**
244
     * Returns the components associated with this context.
245
     *
246
     * @return a list of components
247
     */
248
    public List<Component> getComponents() {
249
        return components;
9✔
250
    }
251

252
    /**
253
     * Returns the component models associated with this context.
254
     *
255
     * @return a map of IRI to model of strings
256
     */
257
    public Map<IRI, IModel<?>> getComponentModels() {
258
        return componentModels;
9✔
259
    }
260

261
    /**
262
     * Returns the introduced IRIs in this context.
263
     *
264
     * @return a set of introduced IRIs
265
     */
266
    public Set<IRI> getIntroducedIris() {
267
        return introducedIris;
×
268
    }
269

270
    /**
271
     * Returns the embedded IRIs in this context.
272
     *
273
     * @return a set of embedded IRIs
274
     */
275
    public Set<IRI> getEmbeddedIris() {
276
        return embeddedIris;
×
277
    }
278

279
    /**
280
     * Returns the role-instantiation direction pins collected in this context, mapping
281
     * each filled/constant role predicate to its pin class
282
     * ({@link com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS#INVERSE_ROLE_PROPERTY}
283
     * or {@code REGULAR_ROLE_PROPERTY}). Emitted into pubinfo at publish time; see #525.
284
     *
285
     * @return a map of predicate IRI to direction-pin class
286
     */
287
    public Map<IRI, IRI> getRolePropertyPins() {
288
        return rolePropertyPins;
×
289
    }
290

291
    /**
292
     * Processes an IRI by applying the template's processing rules.
293
     *
294
     * @param iri the IRI to process
295
     * @return the processed IRI, or null if the processing results in no value
296
     */
297
    public IRI processIri(IRI iri) {
298
        Value v = processValue(iri);
12✔
299
        if (v == null) return null;
6!
300
        if (v instanceof IRI) return (IRI) v;
18!
301
        return iri;
×
302
    }
303

304
    /**
305
     * Processes a Value according to the template's rules.
306
     *
307
     * @param value the Value to process
308
     * @return the processed Value, or the original Value if no processing is applicable
309
     */
310
    public Value processValue(Value value) {
311
        if (!(value instanceof IRI)) {
9!
312
            return value;
×
313
        }
314
        IRI iri = (IRI) value;
9✔
315
        if (iri.equals(NTEMPLATE.CREATOR_PLACEHOLDER)) {
12!
316
            iri = NanodashSession.get().getUserIri();
×
317
        }
318
        if (iri.equals(NTEMPLATE.ASSERTION_PLACEHOLDER)) {
12!
319
            iri = vf.createIRI(targetNamespace + "assertion");
×
320
        } else if (iri.equals(NTEMPLATE.NANOPUB_PLACEHOLDER)) {
12!
321
            iri = vf.createIRI(targetNamespace);
×
322
        } else if (template.isRootNanopubPlaceholder(iri)) {
15✔
323
            IModel<?> rootModel = componentModels.get(iri);
18✔
324
            String rootValue = (rootModel == null || rootModel.getObject() == null) ? "" : rootModel.getObject().toString();
33!
325
            if (fillMode == FillMode.DERIVE) {
12!
326
                // Deriving creates a new resource, so the new nanopub becomes its own root
327
                // definition rather than keeping the source's root (issue #527).
328
                iri = vf.createIRI(targetNamespace);
×
329
            } else if (rootValue.equals(LocalUri.of("nanopub").stringValue())) {
18✔
330
                // Sentinel: in supersede/override mode this means the existing nanopub was the root
331
                Nanopub ref = getReferenceNanopub();
9✔
332
                if (ref != null && (fillMode == FillMode.SUPERSEDE || fillMode == FillMode.OVERRIDE)) {
18!
333
                    iri = vf.createIRI(ref.getUri().stringValue());
21✔
334
                } else {
335
                    iri = vf.createIRI(targetNamespace);
15✔
336
                }
337
            } else if (rootValue.isEmpty()) {
12✔
338
                iri = vf.createIRI(targetNamespace);
18✔
339
            } else {
340
                iri = vf.createIRI(rootValue);
12✔
341
            }
342
        }
343
        // TODO: Move this code below to the respective placeholder classes:
344
        IModel<?> tf = componentModels.get(iri);
18✔
345
        Value processedValue = null;
6✔
346
        Object tfObjectGeneric = null;
6✔
347
        if (tf != null) {
6✔
348
            tfObjectGeneric = tf.getObject();
9✔
349
        }
350
        if (template.isRestrictedChoicePlaceholder(iri)) {
15✔
351
            String tfObject = (String) tfObjectGeneric;
9✔
352
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
21!
353
                String prefix = template.getPrefix(iri);
15✔
354
                if (prefix == null) prefix = "";
12!
355
                if (template.isLocalResource(iri)) prefix = targetNamespace;
15!
356
                if (tfObject.matches("https?://.+")) prefix = "";
12!
357
                String v = prefix + tf.getObject();
18✔
358
                if (v.matches("[^:# ]+")) v = targetNamespace + v;
27!
359
                if (v.matches("https?://.*")) {
12!
360
                    processedValue = vf.createIRI(v);
15✔
361
                } else {
362
                    processedValue = vf.createLiteral(tfObject);
×
363
                }
364
            }
365
        } else if (template.isUriPlaceholder(iri)) {
18✔
366
            String tfObject = (String) tfObjectGeneric;
9✔
367
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
21!
368
                String prefix = template.getPrefix(iri);
15✔
369
                if (prefix == null) prefix = "";
12!
370
                if (template.isLocalResource(iri)) prefix = targetNamespace;
24✔
371
                String v;
372
                if (template.isAutoEscapePlaceholder(iri)) {
15!
373
                    v = prefix + Utils.urlEncode(tf.getObject());
×
374
                } else {
375
                    if (tfObject.matches("https?://.+")) prefix = "";
18✔
376
                    v = prefix + tf.getObject();
18✔
377
                }
378
                if (v.matches("[^:# ]+")) v = targetNamespace + v;
12!
379
                processedValue = vf.createIRI(v);
12✔
380
            }
381
        } else if (template.isLocalResource(iri)) {
18!
382
            String tfObject = (String) tfObjectGeneric;
×
383
            if (template.isIntroducedResource(iri) && (fillMode == FillMode.SUPERSEDE || fillMode == FillMode.OVERRIDE)) {
×
384
                if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
385
                    processedValue = vf.createIRI(tfObject);
×
386
                }
387
            } else {
388
                String prefix = Utils.getUriPrefix(iri);
×
389
                processedValue = vf.createIRI(iri.stringValue().replace(prefix, targetNamespace));
×
390
            }
391
        } else if (template.isLiteralPlaceholder(iri)) {
15✔
392
            IRI datatype = template.getDatatype(iri);
15✔
393
            String languageTag = template.getLanguageTag(iri);
15✔
394
            if (datatype != null) {
6!
395
                if (datatype.equals(XSD.DATETIME)) {
×
396
                    ZonedDateTime tfObject = (ZonedDateTime) tfObjectGeneric;
×
397
                    if (tfObject != null) {
×
398
                        processedValue = vf.createLiteral(tfObject);
×
399
                    }
400
                } else if (datatype.equals(XSD.DATE)) {
×
401
                    Date tfObject = (Date) tfObjectGeneric;
×
402
                    if (tfObject != null) {
×
403
                        processedValue = vf.createLiteral(LiteralDateItem.format.format(tfObject), datatype);
×
404
                    }
405
                }
×
406
            } else {
407
                String tfObject = (String) tfObjectGeneric;
9✔
408
                if (tf != null && tfObject != null && !tfObject.isEmpty()) {
21!
409
                    if (datatype != null) {
6!
410
                        processedValue = vf.createLiteral(tfObject, datatype);
×
411
                    } else if (languageTag != null) {
6!
412
                        processedValue = vf.createLiteral(tfObject, languageTag);
×
413
                    } else {
414
                        processedValue = vf.createLiteral(tfObject);
12✔
415
                    }
416
                }
417
            }
418
        } else if (template.isValuePlaceholder(iri)) {
18!
419
            String tfObject = (String) tfObjectGeneric;
×
420
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
421
                if (Utils.isValidLiteralSerialization(tfObject)) {
×
422
                    processedValue = Utils.getParsedLiteral(tfObject);
×
423
                } else {
424
                    String v = tfObject;
×
425
                    if (v.matches("[^:# ]+")) v = targetNamespace + v;
×
426
                    processedValue = vf.createIRI(v);
×
427
                }
428
            }
429
        } else if (template.isSequenceElementPlaceholder(iri)) {
15!
430
            String tfObject = (String) tfObjectGeneric;
×
431
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
432
                processedValue = vf.createIRI(tfObject);
×
433
            }
434
        } else {
×
435
            processedValue = iri;
6✔
436
        }
437
        if (processedValue instanceof IRI pvIri && template.isIntroducedResource(iri)) {
33!
438
            introducedIris.add(pvIri);
×
439
        }
440
        if (processedValue instanceof IRI pvIri && template.isEmbeddedResource(iri)) {
33!
441
            embeddedIris.add(pvIri);
×
442
        }
443
        if (processedValue instanceof IRI pvIri) {
18✔
444
            IRI directionPin = template.getRoleDirectionPin(iri);
15✔
445
            if (directionPin != null) rolePropertyPins.put(pvIri, directionPin);
6!
446
        }
447
        return processedValue;
6✔
448
    }
449

450
    /**
451
     * Returns the statement items associated with this context.
452
     *
453
     * @return a list of StatementItem objects
454
     */
455
    public List<StatementItem> getStatementItems() {
456
        return statementItems;
×
457
    }
458

459
    /**
460
     * Propagates the statements from this context to a NanopubCreator.
461
     *
462
     * @param npCreator the NanopubCreator to which the statements will be added
463
     * @throws org.nanopub.MalformedNanopubException        if there is an error in the nanopub structure
464
     * @throws org.nanopub.NanopubAlreadyFinalizedException if the nanopub has already been finalized
465
     */
466
    public void propagateStatements(NanopubCreator npCreator) throws MalformedNanopubException, NanopubAlreadyFinalizedException {
467
        if (template.getNanopub() instanceof NanopubWithNs) {
×
468
            NanopubWithNs np = (NanopubWithNs) template.getNanopub();
×
469
            for (String p : np.getNsPrefixes()) {
×
470
                npCreator.addNamespace(p, np.getNamespace(p));
×
471
            }
×
472
        }
473
        for (StatementItem si : statementItems) {
×
474
            si.addTriplesTo(npCreator);
×
475
        }
×
476
    }
×
477

478
    /**
479
     * Checks if the context has a narrow scope for the given IRI.
480
     *
481
     * @param iri the IRI to check
482
     * @return true if there is a narrow scope for the IRI, false otherwise
483
     */
484
    public boolean hasNarrowScope(IRI iri) {
485
        return narrowScopeMap.containsKey(iri);
15✔
486
    }
487

488
    /**
489
     * Checks if any of the statement items in this context will match any triple.
490
     *
491
     * @return true if any statement item will match any triple, false otherwise
492
     */
493
    public boolean willMatchAnyTriple() {
494
        initStatements();
×
495
        for (StatementItem si : statementItems) {
×
496
            if (si.willMatchAnyTriple()) return true;
×
497
        }
×
498
        return false;
×
499
    }
500

501
    /**
502
     * Fills the context with statements, processing each StatementItem.
503
     *
504
     * @param statements the list of statements to fill
505
     * @throws UnificationException if there is an error during unification of statements
506
     */
507
    public void fill(List<Statement> statements) throws UnificationException {
508
        for (StatementItem si : statementItems) {
33✔
509
            // Isolate each statement: a unification failure on one must not abort the rest,
510
            // otherwise every later template statement is left unmatched. Roll back any partial
511
            // statement consumption so the next statement sees an intact pool.
512
            List<Statement> statementsBefore = new ArrayList<>(statements);
15✔
513
            try {
514
                si.fill(statements);
9✔
515
            } catch (UnificationException ex) {
×
516
                logger.warn("Could not fill statement; continuing with the remaining statements", ex);
×
517
                statements.clear();
×
518
                statements.addAll(statementsBefore);
×
519
            }
3✔
520
        }
3✔
521
        for (StatementItem si : statementItems) {
33✔
522
            si.fillFinished();
6✔
523
        }
3✔
524
    }
3✔
525

526
    /**
527
     * Returns the existing Nanopub associated with this context, if any.
528
     *
529
     * @return the existing Nanopub, or null if this context is for a new Nanopub
530
     */
531
    public Nanopub getExistingNanopub() {
532
        return existingNanopub;
9✔
533
    }
534

535
    /**
536
     * Returns the nanopub being used to fill this context (e.g. in supersede/derive
537
     * mode), if any. Unlike {@link #getExistingNanopub()} this does not imply the
538
     * context is read-only.
539
     *
540
     * @return the fill source nanopub, or null if none
541
     */
542
    public Nanopub getFillSource() {
543
        return fillSource;
9✔
544
    }
545

546
    /**
547
     * Sets the nanopub used to fill this context (e.g. in supersede/derive mode).
548
     *
549
     * @param fillSource the nanopub providing the values, or null to clear
550
     */
551
    public void setFillSource(Nanopub fillSource) {
552
        this.fillSource = fillSource;
9✔
553
    }
3✔
554

555
    /**
556
     * Returns the existing or fill-source nanopub for this context, preferring the
557
     * existing nanopub if set.
558
     *
559
     * @return the reference nanopub, or null if none
560
     */
561
    public Nanopub getReferenceNanopub() {
562
        return existingNanopub != null ? existingNanopub : fillSource;
18!
563
    }
564

565
    /**
566
     * Checks if this context is read-only.
567
     *
568
     * @return true if the context is read-only, false otherwise
569
     */
570
    public boolean isReadOnly() {
571
        return existingNanopub != null;
21✔
572
    }
573

574
    /**
575
     * Returns the label for a given IRI, if available.
576
     *
577
     * @param iri the IRI for which to get the label
578
     * @return the label as a String, or null if no label is found
579
     */
580
    public String getLabel(IRI iri) {
581
        if (existingNanopub == null) return null;
×
582
        if (labels == null) {
×
583
            labels = new HashMap<>();
×
584
            for (Statement st : existingNanopub.getPubinfo()) {
×
585
                if (st.getPredicate().equals(NTEMPLATE.HAS_LABEL_FROM_API) || st.getPredicate().equals(RDFS.LABEL)) {
×
586
                    String label = st.getObject().stringValue();
×
587
                    labels.put((IRI) st.getSubject(), label);
×
588
                }
589
            }
×
590
        }
591
        return labels.get(iri);
×
592
    }
593

594
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc