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

knowledgepixels / nanopub-query / 25001159634

27 Apr 2026 02:32PM UTC coverage: 56.911% (+0.3%) from 56.577%
25001159634

Pull #90

github

web-flow
Merge bf9d14c84 into 8f22e9f64
Pull Request #90: feat: materialize canonical foaf:name per agent into trust + spaces graphs (#62)

425 of 842 branches covered (50.48%)

Branch coverage included in aggregate %.

1189 of 1994 relevant lines covered (59.63%)

8.98 hits per line

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

69.9
src/main/java/com/knowledgepixels/query/TrustStateSnapshot.java
1
package com.knowledgepixels.query;
2

3
import java.time.Instant;
4
import java.time.ZonedDateTime;
5
import java.util.ArrayList;
6
import java.util.Collections;
7
import java.util.List;
8

9
import io.vertx.core.json.JsonArray;
10
import io.vertx.core.json.JsonObject;
11

12
/**
13
 * Parsed envelope of a {@code /trust-state/<hash>.json} response from the
14
 * registry. Immutable; produced by {@link #parse(String)}.
15
 *
16
 * <p>The {@code trustStateCounter} field arrives as MongoDB extended JSON
17
 * (e.g. {@code {"$numberLong": "1"}}) when the registry's serializer chooses
18
 * to wrap a long. The parser handles either wrapped or plain numeric form.
19
 *
20
 * <p>The {@code createdAt} field uses Java's {@code ZonedDateTime.toString()}
21
 * shape — ISO-8601 plus an optional {@code [Etc/UTC]} zone bracket — which
22
 * {@link ZonedDateTime#parse(CharSequence)} reads natively.
23
 *
24
 * @param trustStateHash the hash advertised by the registry for this snapshot
25
 * @param trustStateCounter monotonic counter; matches the registry's value
26
 * @param createdAt when the registry committed this snapshot
27
 * @param accounts one entry per non-{@code "$"} account in the trust graph
28
 */
29
public record TrustStateSnapshot(
45✔
30
        String trustStateHash,
31
        long trustStateCounter,
32
        Instant createdAt,
33
        List<AccountEntry> accounts
34
) {
35

36
    /**
37
     * One account in a trust state snapshot. Mirrors the fields the registry
38
     * exposes; consumers (e.g. authority queries) decide which {@code status}
39
     * values count as "approved".
40
     *
41
     * <p>{@code pathCount}, {@code ratio}, and {@code quota} can be {@code null}
42
     * for accounts with {@code status == "skipped"} (trust calculation rejected
43
     * them, so those stats aren't meaningful). {@code name} and {@code nameCreatedAt}
44
     * are {@code null} when the declaring intro nanopub asserted no
45
     * {@code foaf:name} on the agent (or when running against a registry that
46
     * predates the field — additive non-breaking schema). Boxed types preserve
47
     * the absence.
48
     *
49
     * @param pubkey hex-encoded public key
50
     * @param agent agent IRI (typically an ORCID, but any IRI is allowed)
51
     * @param status one of the registry's {@code EntryStatus} values
52
     *               (e.g. {@code "loaded"}, {@code "toLoad"}, {@code "skipped"})
53
     * @param depth steps from the trust seed
54
     * @param pathCount number of independent trust paths, or {@code null} for skipped accounts
55
     * @param ratio aggregated trust score, or {@code null} for skipped accounts
56
     * @param quota allocated upload quota, or {@code null} for skipped accounts
57
     * @param name {@code foaf:name} from the declaring intro nanopub, or {@code null} if absent
58
     * @param nameCreatedAt {@code dct:created} of the declaring intro, or {@code null} if absent.
59
     *                      Used to break ties when multiple intros declare the same {@code (agent, pubkey)};
60
     *                      consumers picking one canonical name per agent across keys typically use
61
     *                      {@code MAX(ratio)} with this as a secondary tiebreaker.
62
     */
63
    public record AccountEntry(
90✔
64
            String pubkey,
65
            String agent,
66
            String status,
67
            Integer depth,
68
            Integer pathCount,
69
            Double ratio,
70
            Long quota,
71
            String name,
72
            Instant nameCreatedAt
73
    ) {}
74

75
    /**
76
     * Parses a {@code /trust-state/<hash>.json} envelope from its JSON
77
     * serialization. Throws {@link IllegalArgumentException} if the JSON is
78
     * malformed or missing required fields.
79
     *
80
     * @param json the response body as a string
81
     * @return the parsed snapshot
82
     * @throws IllegalArgumentException if parsing fails
83
     */
84
    public static TrustStateSnapshot parse(String json) {
85
        JsonObject obj;
86
        try {
87
            obj = new JsonObject(json);
15✔
88
        } catch (Exception ex) {
3✔
89
            throw new IllegalArgumentException("Trust state envelope is not valid JSON", ex);
18✔
90
        }
3✔
91

92
        String hash = requireString(obj, "trustStateHash");
12✔
93
        long counter = unwrapLong(obj, "trustStateCounter");
12✔
94

95
        String rawCreatedAt = requireString(obj, "createdAt");
12✔
96
        Instant createdAt;
97
        try {
98
            createdAt = ZonedDateTime.parse(rawCreatedAt).toInstant();
12✔
99
        } catch (Exception ex) {
3✔
100
            throw new IllegalArgumentException(
21✔
101
                    "Cannot parse createdAt as ZonedDateTime: " + rawCreatedAt, ex);
102
        }
3✔
103

104
        JsonArray accountsArray = obj.getJsonArray("accounts");
12✔
105
        if (accountsArray == null) {
6✔
106
            throw new IllegalArgumentException("Trust state envelope is missing 'accounts' array");
15✔
107
        }
108
        List<AccountEntry> accounts = new ArrayList<>(accountsArray.size());
18✔
109
        for (int i = 0; i < accountsArray.size(); i++) {
24✔
110
            JsonObject a;
111
            try {
112
                a = accountsArray.getJsonObject(i);
12✔
113
            } catch (ClassCastException ex) {
×
114
                throw new IllegalArgumentException(
×
115
                        "Trust state account entry " + i + " is not a JSON object", ex);
116
            }
3✔
117
            accounts.add(new AccountEntry(
21✔
118
                    requireString(a, "pubkey"),
9✔
119
                    requireString(a, "agent"),
9✔
120
                    requireString(a, "status"),
9✔
121
                    a.getInteger("depth"),
9✔
122
                    a.getInteger("pathCount"),
9✔
123
                    a.getDouble("ratio"),
9✔
124
                    unwrapLongNullable(a, "quota"),
9✔
125
                    a.getString("name"),
9✔
126
                    unwrapDateNullable(a, "nameCreatedAt")
6✔
127
            ));
128
        }
129

130
        return new TrustStateSnapshot(hash, counter, createdAt,
21✔
131
                Collections.unmodifiableList(accounts));
6✔
132
    }
133

134
    private static String requireString(JsonObject obj, String key) {
135
        String s = obj.getString(key);
12✔
136
        if (s == null) {
6✔
137
            throw new IllegalArgumentException("Trust state envelope is missing required field: " + key);
18✔
138
        }
139
        return s;
6✔
140
    }
141

142
    /**
143
     * Reads a required long-typed field, transparently handling MongoDB
144
     * extended JSON ({@code {"$numberLong": "..."}}) as well as plain numeric
145
     * or string forms. Throws if the field is missing or null.
146
     */
147
    private static long unwrapLong(JsonObject obj, String key) {
148
        Long v = unwrapLongNullable(obj, key);
12✔
149
        if (v == null) {
6!
150
            throw new IllegalArgumentException("Trust state envelope is missing required field: " + key);
×
151
        }
152
        return v;
9✔
153
    }
154

155
    /**
156
     * Reads an optional date-typed field. Handles MongoDB extended JSON
157
     * ({@code {"$date": "..."}} or {@code {"$date": {"$numberLong": "..."}}}),
158
     * plain ISO-8601 strings, and JSON {@code null}/missing. Returns {@code null}
159
     * for any of those last cases so the field is fully optional — consumers
160
     * working with snapshots from a registry that predates the {@code nameCreatedAt}
161
     * field still parse cleanly.
162
     */
163
    private static Instant unwrapDateNullable(JsonObject obj, String key) {
164
        Object v = obj.getValue(key);
12✔
165
        if (v == null) return null;
12✔
166
        if (v instanceof JsonObject j) {
18✔
167
            Object dateVal = j.getValue("$date");
12✔
168
            if (dateVal instanceof String s) {
18!
169
                try {
170
                    return Instant.parse(s);
9✔
171
                } catch (Exception ex) {
×
172
                    throw new IllegalArgumentException("Cannot parse " + key + " as ISO-8601 instant: " + s, ex);
×
173
                }
174
            }
175
            if (dateVal instanceof JsonObject inner) {
×
176
                String numberLong = inner.getString("$numberLong");
×
177
                if (numberLong != null) return Instant.ofEpochMilli(Long.parseLong(numberLong));
×
178
            }
179
            if (dateVal instanceof Number n) return Instant.ofEpochMilli(n.longValue());
×
180
            throw new IllegalArgumentException("Cannot unwrap " + key + " from extended JSON $date: " + j);
×
181
        }
182
        if (v instanceof String s) {
18!
183
            try {
184
                return Instant.parse(s);
9✔
185
            } catch (Exception ex) {
×
186
                throw new IllegalArgumentException("Cannot parse " + key + " as ISO-8601 instant: " + s, ex);
×
187
            }
188
        }
189
        if (v instanceof Number n) return Instant.ofEpochMilli(n.longValue());
×
190
        throw new IllegalArgumentException("Cannot unwrap " + key + " as date: " + v);
×
191
    }
192

193
    /**
194
     * Reads an optional long-typed field, returning {@code null} if the field
195
     * is absent or its value is JSON {@code null}. Same extended-JSON handling
196
     * as {@link #unwrapLong(JsonObject, String)}.
197
     */
198
    private static Long unwrapLongNullable(JsonObject obj, String key) {
199
        Object v = obj.getValue(key);
12✔
200
        if (v == null) return null;
12✔
201
        if (v instanceof Number n) return n.longValue();
30✔
202
        if (v instanceof JsonObject j) {
18!
203
            String s = j.getString("$numberLong");
12✔
204
            if (s != null) return Long.parseLong(s);
18!
205
        }
206
        if (v instanceof String s) return Long.parseLong(s);
×
207
        throw new IllegalArgumentException("Cannot unwrap " + key + " as long: " + v);
×
208
    }
209

210
}
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