Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion odp-directory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ details. An imported OpenAPI Collection is a Directory presentation group, not a
`getCollection` target. `indexedAt()` on the result records Collection freshness, while
`service().indexedAt()` records the parent's freshness. `service().serviceId()` identifies the
local Directory Service.
For Service results, optional `availableThrough()` identifies a platform. Collection attribution
For Service results, optional `publisher()` provides `publisherId()`, `name()`, and `websiteUrl()` for display attribution.
The website is not a discovery or execution target. Omitted or null attribution is accepted;
additional response fields are retained. Collection attribution
is its owning `service()`.

Known types are validated; a malformed item is omitted and reported in `response.issues()` with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public sealed interface Result permits ServiceResult, CollectionResult, UnknownR
}

public record ServiceResult(
Service service, Instant indexedAt, ServiceReference availableThrough, Map<String, OdpJsonNode> additional)
Service service, Instant indexedAt, Publisher publisher, Map<String, OdpJsonNode> additional)
implements Result {
public ServiceResult {
additional = Collections.unmodifiableMap(new LinkedHashMap<>(additional));
Expand Down Expand Up @@ -80,9 +80,8 @@ public OdpJsonNode raw() {
}
}

public record ServiceReference(
String serviceId, String serviceOrigin, String name, Map<String, OdpJsonNode> additional) {
public ServiceReference {
public record Publisher(String publisherId, String name, String websiteUrl, Map<String, OdpJsonNode> additional) {
public Publisher {
additional = Collections.unmodifiableMap(new LinkedHashMap<>(additional));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class DirectoryResults {
private static final String FIELD_SERVICE_ID = "service_id";
private static final String FIELD_SERVICE_ORIGIN = "service_origin";
private static final String FIELD_INDEXED_AT = "indexed_at";
private static final String FIELD_AVAILABLE_THROUGH = "available_through";
private static final String FIELD_PUBLISHER = "publisher";
private static final String FIELD_NAME = "name";

private DirectoryResults() {}
Expand Down Expand Up @@ -75,13 +75,15 @@ private static DirectoryModels.Result result(OdpJsonNode value) {
DirectoryModels.Service service = OdpJson.treeToValue(serviceNode, DirectoryModels.Service.class);
Instant indexedAt = instant(value, FIELD_INDEXED_AT);
if (FIELD_SERVICE.equals(type)) {
DirectoryModels.ServiceReference reference =
value.has(FIELD_AVAILABLE_THROUGH) ? reference(value.get(FIELD_AVAILABLE_THROUGH)) : null;
DirectoryModels.Publisher publisher =
value.has(FIELD_PUBLISHER) && !value.get(FIELD_PUBLISHER).isNull()
? publisher(value.get(FIELD_PUBLISHER))
: null;
return new DirectoryModels.ServiceResult(
service,
indexedAt,
reference,
additional(value, Set.of("type", FIELD_SERVICE, FIELD_INDEXED_AT, FIELD_AVAILABLE_THROUGH)));
publisher,
additional(value, Set.of("type", FIELD_SERVICE, FIELD_INDEXED_AT, FIELD_PUBLISHER)));
}
return collection(value, service, indexedAt);
}
Expand Down Expand Up @@ -126,13 +128,18 @@ private static DirectoryModels.CollectionResult collection(
additional(value, Set.of("type", FIELD_SERVICE, FIELD_INDEXED_AT, FIELD_COLLECTION)));
}

private static DirectoryModels.ServiceReference reference(OdpJsonNode value) {
object(value, FIELD_AVAILABLE_THROUGH);
return new DirectoryModels.ServiceReference(
text(value, FIELD_SERVICE_ID, 128),
origin(value, FIELD_SERVICE_ORIGIN),
value.has(FIELD_NAME) ? text(value, FIELD_NAME, 128) : null,
additional(value, Set.of(FIELD_SERVICE_ID, FIELD_SERVICE_ORIGIN, FIELD_NAME)));
private static DirectoryModels.Publisher publisher(OdpJsonNode value) {
object(value, FIELD_PUBLISHER);
String website = text(value, "website_url", 512);
URI uri = URI.create(website);
if (!"https".equals(uri.getScheme()) || uri.getHost() == null || uri.getRawUserInfo() != null) {
throw new IllegalArgumentException("Publisher website must be an HTTPS URL without credentials");
}
return new DirectoryModels.Publisher(
text(value, "publisher_id", 128),
text(value, FIELD_NAME, 128),
website,
additional(value, Set.of("publisher_id", "website_url", FIELD_NAME)));
}

private static String origin(OdpJsonNode value, String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ static String response(OdpJsonNode... items) {
@Test
void readsKnownAndUnknownResultsWithAttributionAndFreshness() {
OdpJsonNode service = result("service");
service.set("available_through", OdpJson.parseTree("""
{"service_id":"platform","service_origin":"https://platform.example","name":"Platform","extra":true}
service.set("publisher", OdpJson.parseTree("""
{"publisher_id":"platform","website_url":"https://platform.example/catalog","name":"Platform","extra":true}
"""));
service.put("extra", "retained");
OdpJsonNode collection = result("collection");
Expand All @@ -58,10 +58,10 @@ void readsKnownAndUnknownResultsWithAttributionAndFreshness() {
var first = assertInstanceOf(
DirectoryModels.ServiceResult.class, decoded.items().get(0));
assertEquals("service", first.type());
assertEquals("Platform", first.availableThrough().name());
assertEquals("Platform", first.publisher().name());
assertEquals("ca0304cc-ab28-43e5-af94-7bdf11b40c6e", first.service().serviceId());
assertEquals("retained", first.additional().get("extra").asString());
assertTrue(first.availableThrough().additional().get("extra").asBoolean(false));
assertTrue(first.publisher().additional().get("extra").asBoolean(false));
assertEquals(1, first.service().protocols().trust().size());
var second = assertInstanceOf(
DirectoryModels.CollectionResult.class, decoded.items().get(1));
Expand Down Expand Up @@ -106,13 +106,14 @@ void reportsMalformedKnownEntriesWithoutLosingValidOnes() {
assertEquals(0, decoded.issues().get(0).index());
}
for (String reference : List.of(
"null",
"{}",
"false",
"{\"service_id\":\"x\",\"service_origin\":\"https://user@platform.example\"}",
"{\"service_id\":\"x\",\"service_origin\":\"https://platform.example\",\"name\":null}")) {
"{\"publisher_id\":\"x\",\"website_url\":\"https://user@platform.example\",\"name\":\"Platform\"}",
"{\"publisher_id\":\"x\",\"website_url\":\"http://platform.example\",\"name\":\"Platform\"}",
"{\"publisher_id\":\"x\",\"website_url\":\"https:///\",\"name\":\"Platform\"}",
"{\"publisher_id\":\"x\",\"website_url\":\"https://platform.example\",\"name\":null}")) {
OdpJsonNode invalid = result("service");
invalid.set("available_through", OdpJson.parseTree(reference));
invalid.set("publisher", OdpJson.parseTree(reference));
assertEquals(1, DirectoryResults.decode(response(invalid)).issues().size());
}
}
Expand All @@ -127,21 +128,32 @@ void acceptsOptionalFieldsAndRemovesUnverifiedExecutionMetadata() {
{"name":"list-offerings","authentication":"not-required"},
{"name":"future-operation","authentication":"not-required"}]
"""));
service.set("available_through", OdpJson.parseTree("""
{"service_id":"x","service_origin":"https://platform.example"}
service.set("publisher", OdpJson.parseTree("""
{"publisher_id":"x","website_url":"https://platform.example","name":"Platform"}
"""));
var decoded = DirectoryResults.decode(response(service));
var item = assertInstanceOf(
DirectoryModels.ServiceResult.class, decoded.items().get(0));
assertNull(item.availableThrough().name());
assertEquals("Platform", item.publisher().name());
assertEquals("x", item.publisher().publisherId());
assertEquals("https://platform.example", item.publisher().websiteUrl());
assertNull(item.service().protocols());
assertNull(item.service().additional().get("http"));
assertEquals(2, item.service().operations().size());
service.remove("available_through");
service.set("publisher", OdpJson.parseTree("null"));
service.set("available_through", OdpJson.parseTree("{\"service_id\":\"legacy\"}"));
service.set("future_metadata", OdpJson.parseTree("{\"arbitrary\":true}"));
var withoutPublisher = assertInstanceOf(
DirectoryModels.ServiceResult.class,
DirectoryResults.decode(response(service)).items().get(0));
assertNull(withoutPublisher.publisher());
assertTrue(withoutPublisher.additional().containsKey("available_through"));
assertTrue(withoutPublisher.additional().containsKey("future_metadata"));
service.remove("publisher");
assertNull(assertInstanceOf(
DirectoryModels.ServiceResult.class,
DirectoryResults.decode(response(service)).items().get(0))
.availableThrough());
.publisher());
for (boolean omit : List.of(true, false)) {
OdpJsonNode collection = result("collection");
if (omit) {
Expand Down
Loading