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 crates/registry-breg-client-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ key.
Ordinary list calls accept `bbox: [west, south, east, north]` as four exact
decimal strings when the served query contract supports it. GeoJSON has a
separate `getGeoJsonRecord` and `listGeoJsonRecords` surface, with typed
features, GeoJSON media checks, and its own continuation type.
features whose geometry is `Point | Polygon | MultiPolygon | null`, GeoJSON
media checks, and its own continuation type. Polygon ring order and winding are
preserved.

`listCurrentRecords`, `listRecordsAsOf`, and `listSnapshotRecords` keep their
temporal parameters and continuations distinct. Snapshot continuations bind
Expand Down
9 changes: 9 additions & 0 deletions crates/registry-breg-client-node/__test__/facade.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
AsOfListContinuation,
CurrentListContinuation,
GeoJsonListContinuation,
GeoJsonGeometry,
RelationshipListContinuation,
SnapshotListContinuation,
ListContinuation,
Expand Down Expand Up @@ -84,6 +85,14 @@ declare const asOfContinuation: AsOfListContinuation
declare const snapshotContinuation: SnapshotListContinuation
declare const relationshipContinuation: RelationshipListContinuation
declare const geoJsonContinuation: GeoJsonListContinuation
declare const geometry: GeoJsonGeometry
if (geometry.type === 'Polygon') {
geometry.coordinates[0]?.[0]?.[0].toFixed()
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates[0]?.[0]?.[0]?.[1].toFixed()
} else {
geometry.coordinates[0].toFixed()
}

metadata.actions[0]?.inputs[0]?.schemaJson.toUpperCase()
metadata.actions[0]?.attachments[0]?.contentTypes[0]?.toUpperCase()
Expand Down
12 changes: 9 additions & 3 deletions crates/registry-breg-client-node/__test__/read-parity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const link = '<https://id.registrystack.org/profiles/registry-record/v1>; rel="profile", </v1/schemas/company>; rel="describedby"';
const meta = { registryIdentifier: 'registry', datasetIdentifier: 'dataset', recordTypeIdentifier: 'company' };
const record = { recordIdentifier: id, revisionIdentifier: '1', domainData: { label: 'one' } };
const feature = { type: 'Feature', id, geometry: null, properties: { wide: 9007199254740992 }, registry: { revision: 1 } };
const polygon = { type: 'Polygon', coordinates: [[[100.25, 13.25], [101, 13.25], [101, 14], [100.25, 13.25]]] };
const multiPolygon = { type: 'MultiPolygon', coordinates: [[[[100.25, 13.25], [101, 13.25], [101, 14], [100.25, 13.25]]]] };
const feature = { type: 'Feature', id, geometry: polygon, properties: { wide: 9007199254740992 }, registry: { revision: 1 } };

function respond(response, mediaType, document, includeLink = false, etag = false) {
response.setHeader('content-type', mediaType);
Expand All @@ -29,7 +31,7 @@ test('specialized read methods keep routes, cursors, snapshots, exact JSON and o
const continuation = request.url.includes('$skiptoken=');
if (request.headers.accept === 'application/geo+json') {
if (path.endsWith(id)) return respond(response, 'application/geo+json', feature);
return respond(response, 'application/geo+json', `{"type":"FeatureCollection","features":[{"type":"Feature","id":"${id}","geometry":null,"properties":{"wide":9007199254740992},"registry":{"revision":1}}],"numberReturned":1,"registry":{"pageInfo":{"nextCursor":${continuation ? 'null' : '"geo-next"'}}}}`);
return respond(response, 'application/geo+json', `{"type":"FeatureCollection","features":[{"type":"Feature","id":"${id}","geometry":${JSON.stringify(multiPolygon)},"properties":{"wide":9007199254740992},"registry":{"revision":1}}],"numberReturned":1,"registry":{"pageInfo":{"nextCursor":${continuation ? 'null' : '"geo-next"'}}}}`);
}
const collection = !path.endsWith(id) && !path.includes('/revisions/');
const responseMeta = path.includes('/incomplete-meta/')
Expand All @@ -55,9 +57,12 @@ test('specialized read methods keep routes, cursors, snapshots, exact JSON and o
await client.getRecordJson('companies', id, { select: ['name'] });
assert(Buffer.isBuffer((await client.getRecordRevision('companies', id, 1)).body));

assert.equal((await client.getGeoJsonRecord('companies', id)).value.type, 'Feature');
const polygonFeature = (await client.getGeoJsonRecord('companies', id)).value;
assert.equal(polygonFeature.geometry.type, 'Polygon');
assert.deepEqual(polygonFeature.geometry.coordinates[0][0], [100.25, 13.25]);
assert.match((await client.getGeoJsonRecordJson('companies', id)).valueJson, /9007199254740992/);
const geo = await client.listGeoJsonRecords('companies', { accessProfile: 'map', bbox: ['100.1', '13.1', '100.2', '13.2'] });
assert.equal(geo.value.features[0].geometry.type, 'MultiPolygon');
await client.continueGeoJsonList(geo.continuation);
const geoJson = await client.listGeoJsonRecordsJson('companies');
assert.match(geoJson.valueJson, /9007199254740992/);
Expand Down Expand Up @@ -101,6 +106,7 @@ test('specialized read methods keep routes, cursors, snapshots, exact JSON and o

const requestCount = requests.length;
await assert.rejects(client.listCurrentRecords('companies', { bbox: ['1', '2', '3', '4'] }), error => error.kind === 'invalid-request');
await assert.rejects(client.listGeoJsonRecords('companies', { bbox: ['100', '13', '99', '14'] }), error => error.kind === 'invalid-request');
await assert.rejects(client.getRecordRevision('companies', id, Number.MAX_SAFE_INTEGER + 1), error => error.kind === 'invalid-request');
assert.equal(requests.length, requestCount);
} finally {
Expand Down
14 changes: 12 additions & 2 deletions crates/registry-breg-client-node/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,14 +351,24 @@ export interface JsonOutcome<C = ListContinuation> {
validAt?: string
}

export type GeoJsonPosition = readonly [longitude: number, latitude: number]
export interface GeoJsonPoint {
readonly type: 'Point'
readonly coordinates: readonly [number, number]
readonly coordinates: GeoJsonPosition
}
export interface GeoJsonPolygon {
readonly type: 'Polygon'
readonly coordinates: ReadonlyArray<ReadonlyArray<GeoJsonPosition>>
}
export interface GeoJsonMultiPolygon {
readonly type: 'MultiPolygon'
readonly coordinates: ReadonlyArray<ReadonlyArray<ReadonlyArray<GeoJsonPosition>>>
}
export type GeoJsonGeometry = GeoJsonPoint | GeoJsonPolygon | GeoJsonMultiPolygon
export interface GeoJsonFeature {
readonly type: 'Feature'
readonly id: string
readonly geometry: GeoJsonPoint | null
readonly geometry: GeoJsonGeometry | null
readonly properties: Readonly<Record<string, JsonValue>>
readonly registry: { readonly revision: SafeInteger }
}
Expand Down
3 changes: 2 additions & 1 deletion crates/registry-breg-client-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ These descriptions support interface choices; select a binding before execution.
`list_records` accepts an optional exact decimal `bbox=(west, south, east,
north)`. GeoJSON has separate `get_geojson_record`, `list_geojson_records`, and
`continue_geojson_list` methods so its response contract cannot be confused
with a Registry Record.
with a Registry Record. Their typed feature geometry is `Point`, `Polygon`,
`MultiPolygon`, or `None`. Polygon ring order and winding are preserved.

Temporal and relationship collections also use operation-specific methods and
continuations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,58 @@ StatisticsFormat = Literal["json", "csv"]
StatisticsReleaseStatus = Literal["provisional", "final"]
StatisticsReleaseSelection = Literal["any", "final"]
StatisticsWithdrawalReason = Literal["computation-error", "source-data-error", "disclosure-risk"]
GeoJsonPosition = list[float | int]

class GeoJsonPoint(TypedDict):
type: Literal["Point"]
coordinates: GeoJsonPosition

class GeoJsonPolygon(TypedDict):
type: Literal["Polygon"]
coordinates: list[list[GeoJsonPosition]]

class GeoJsonMultiPolygon(TypedDict):
type: Literal["MultiPolygon"]
coordinates: list[list[list[GeoJsonPosition]]]

GeoJsonGeometry = GeoJsonPoint | GeoJsonPolygon | GeoJsonMultiPolygon

class GeoJsonFeatureRegistry(TypedDict):
revision: int

class GeoJsonFeature(TypedDict):
type: Literal["Feature"]
id: str
geometry: GeoJsonGeometry | None
properties: dict[str, JsonValue]
registry: GeoJsonFeatureRegistry

class GeoJsonPageInfo(TypedDict):
nextCursor: str | None

class GeoJsonCollectionRegistry(TypedDict):
pageInfo: GeoJsonPageInfo
count: NotRequired[int]

class GeoJsonFeatureCollection(TypedDict):
type: Literal["FeatureCollection"]
features: list[GeoJsonFeature]
numberReturned: int
registry: GeoJsonCollectionRegistry

class GeoJsonCompleteOutcome(TypedDict):
kind: Literal["complete"]
value: GeoJsonFeature
trace_id: str
etag: str | None
location: str | None

class GeoJsonPageOutcome(TypedDict):
kind: Literal["complete"]
value: GeoJsonFeatureCollection
continuation: dict[str, JsonValue] | None
trace_id: str
etag: str | None

class BRegStatisticalDimensionDescriptor(TypedDict):
field: str
Expand Down Expand Up @@ -389,7 +441,7 @@ class BaseRegistryClient:
*,
select: Sequence[str] | None = None,
access_profile: str | None = None,
) -> dict[str, Any]: ...
) -> GeoJsonCompleteOutcome: ...
def list_geojson_records(
self,
record_route: str,
Expand All @@ -401,10 +453,10 @@ class BaseRegistryClient:
orderby: str | None = None,
count: bool | None = None,
bbox: tuple[str, str, str, str] | None = None,
) -> dict[str, Any]: ...
) -> GeoJsonPageOutcome: ...
def continue_geojson_list(
self, continuation: dict[str, JsonValue]
) -> dict[str, Any]: ...
) -> GeoJsonPageOutcome: ...
def record_revisions(
self,
record_route: str,
Expand Down
43 changes: 39 additions & 4 deletions crates/registry-breg-client-py/tests/python/test_read_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ def metadata() -> dict:
}


def feature() -> dict:
def feature(geometry: dict | None = None) -> dict:
return {
"type": "Feature",
"id": RECORD_ID,
"geometry": {"type": "Point", "coordinates": [100.25, 13.25]},
"geometry": geometry
or {
"type": "Polygon",
"coordinates": [
[[100.25, 13.25], [101, 13.25], [101, 14], [100.25, 13.25]]
],
},
"properties": {"label": "one", "large": 2**63},
"registry": {"revision": 1},
}
Expand Down Expand Up @@ -76,7 +82,23 @@ def do_GET(self) -> None: # noqa: N802
self.respond(
{
"type": "FeatureCollection",
"features": [feature()],
"features": [
feature(
{
"type": "MultiPolygon",
"coordinates": [
[
[
[100.25, 13.25],
[101, 13.25],
[101, 14],
[100.25, 13.25],
]
]
],
}
)
],
"numberReturned": 1,
"registry": {
"pageInfo": {"nextCursor": cursor},
Expand Down Expand Up @@ -151,7 +173,10 @@ def tearDown(self) -> None:

def test_geojson_bbox_and_typed_continuation(self) -> None:
single = self.client.get_geojson_record("companies", RECORD_ID)
self.assertEqual(single["value"]["geometry"]["coordinates"], [100.25, 13.25])
self.assertEqual(single["value"]["geometry"]["type"], "Polygon")
self.assertEqual(
single["value"]["geometry"]["coordinates"][0][0], [100.25, 13.25]
)
self.assertEqual(single["value"]["properties"]["large"], 2**63)
self.assertIsNone(single["etag"])

Expand All @@ -163,10 +188,20 @@ def test_geojson_bbox_and_typed_continuation(self) -> None:
bbox=("100.1", "13.1", "100.2", "13.2"),
)
self.assertEqual(first["value"]["numberReturned"], 1)
self.assertEqual(
first["value"]["features"][0]["geometry"]["type"], "MultiPolygon"
)
self.assertEqual(first["continuation"]["skiptoken"], "geo-next")
second = self.client.continue_geojson_list(first["continuation"])
self.assertIsNone(second["continuation"])
self.assertTrue(any("bbox=100.1%2C13.1%2C100.2%2C13.2" in path for path, _ in self.requests))
request_count = len(self.requests)
with self.assertRaises(BaseRegistryClientError) as invalid_bbox:
self.client.list_geojson_records(
"companies", bbox=("100", "13", "99", "14")
)
self.assertEqual(invalid_bbox.exception.kind, "invalid-request")
self.assertEqual(len(self.requests), request_count)

def test_temporal_snapshot_relationship_revision_reads(self) -> None:
current = self.client.list_current_records("companies", top=1)
Expand Down
10 changes: 6 additions & 4 deletions crates/registry-breg-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ same collection identity.

## Native collection capabilities

Native Point queries use `BRegBoundingBox` and `BRegListRequest::bbox` for JSON,
Native Point and polygon queries use `BRegBoundingBox` and `BRegListRequest::bbox` for JSON,
or the separate `get_geojson_record`, `list_geojson_records`, and
`continue_geojson_list` methods for GeoJSON. Boxes contain exact decimal strings
in west, south, east, north order. Edges are inclusive; zero-area boxes are valid.
in west, south, east, north order. Edges are inclusive and zero-area boxes are valid.
The client validates coordinate and wire bounds, and the server enforces the
profile's geometry/query grants and span limits. GeoJSON get responses have no
mutation ETag. The `/v1/gis` QGIS adapter routes are outside SDK scope.
profile's geometry/query grants and span limits. Typed GeoJSON features retain
`Point`, `Polygon`, and `MultiPolygon` coordinates, including ring order and
winding. Read the native Record representation for a mutation ETag. QGIS
connects directly to the `/v1/gis` adapter.

The runnable `native_geojson` example prints each FeatureCollection as one JSON
line. Run it against an existing spatial registry from the workspace root,
Expand Down
2 changes: 1 addition & 1 deletion crates/registry-breg-client/examples/native_geojson.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0

//! Read a configured Point collection with a bbox and explicit pagination.
//! Read a configured spatial collection with a bbox and explicit pagination.
//! See the crate README for the required environment variables.

use std::sync::Arc;
Expand Down
Loading
Loading