diff --git a/src/c3nav/api/ninja.py b/src/c3nav/api/ninja.py index f3199eae..7fcd0ea0 100644 --- a/src/c3nav/api/ninja.py +++ b/src/c3nav/api/ninja.py @@ -22,8 +22,7 @@ class SwaggerAndRedoc(DocsBase): redoc_config = Redoc(settings={ "hideOneOfDescription": True, "expandSingleSchemaField": True, - "jsonSampleExpandLevel": 4, - "showObjectSchemaExamples": True, + "jsonSampleExpandLevel": 5, "expandResponses": "200", "hideSingleRequestSampleTab": True, "nativeScrollbars": True, diff --git a/src/c3nav/api/schema.py b/src/c3nav/api/schema.py index 38bee813..c3b4c7ce 100644 --- a/src/c3nav/api/schema.py +++ b/src/c3nav/api/schema.py @@ -25,6 +25,9 @@ class PolygonSchema(Schema): example=[[[1.5, 1.5], [1.5, 2.5], [2.5, 2.5], [2.5, 2.5]]] ) + class Config(Schema.Config): + title = "GeoJSON Polygon" + class LineStringSchema(Schema): """ @@ -35,6 +38,9 @@ class LineStringSchema(Schema): example=[[1.5, 1.5], [2.5, 2.5], [5, 8.7]] ) + class Config(Schema.Config): + title = "GeoJSON LineString" + class LineSchema(Schema): """ @@ -45,6 +51,9 @@ class LineSchema(Schema): example=[[1.5, 1.5], [5, 8.7]] ) + class Config(Schema.Config): + title = "GeoJSON LineString (only two points)" + class PointSchema(Schema): """ @@ -55,6 +64,9 @@ class PointSchema(Schema): example=[1, 2.5] ) + class Config(Schema.Config): + title = "GeoJSON Point" + GeometrySchema = Annotated[ Union[ diff --git a/src/c3nav/editor/api/endpoints.py b/src/c3nav/editor/api/endpoints.py index 1c838f67..a02dddc3 100644 --- a/src/c3nav/editor/api/endpoints.py +++ b/src/c3nav/editor/api/endpoints.py @@ -10,14 +10,14 @@ from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, Geome from c3nav.editor.views.base import editor_etag_func from c3nav.mapdata.api.base import api_etag from c3nav.mapdata.models import Source -from c3nav.mapdata.schemas.responses import BoundsSchema +from c3nav.mapdata.schemas.responses import WithBoundsSchema editor_api_router = APIRouter(tags=["editor"], auth=APITokenAuth(permissions={"editor_access"})) @editor_api_router.get('/bounds/', summary="boundaries", description="get maximum boundaries of everything on the map", - response={200: BoundsSchema, **auth_permission_responses}, + response={200: WithBoundsSchema, **auth_permission_responses}, openapi_extra={"security": [{"APITokenAuth": ["editor_access"]}]}) @api_etag() def bounds(request): diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index 9d4250c0..4c0d64c0 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -17,10 +17,11 @@ from c3nav.mapdata.api.base import api_etag, api_stats from c3nav.mapdata.models import Source from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter -from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID +from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID, schema_definition from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema, - LocationDisplay, SlimListableLocationSchema, SlimLocationSchema) -from c3nav.mapdata.schemas.responses import BoundsSchema, LocationGeometry + LevelSchema, LocationDisplay, SlimListableLocationSchema, SlimLocationSchema, + all_location_definitions, listable_location_definitions) +from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, searchable_locations_for_request, visible_locations_for_request) from c3nav.mapdata.utils.user import can_access_editor @@ -30,7 +31,7 @@ map_api_router = APIRouter(tags=["map"]) @map_api_router.get('/bounds/', summary="get boundaries", description="get maximum boundaries of everything on the map", - response={200: BoundsSchema, **auth_responses}) + response={200: WithBoundsSchema, **auth_responses}) @api_etag(permissions=False) def bounds(request): return { @@ -69,7 +70,8 @@ def _location_list(request, detailed: bool, filters: LocationListFilters): @map_api_router.get('/locations/', summary="list locations (slim)", - description="Get locations (with most important attributes set)", + description=("Get locations (with most important attributes set)\n\n" + "Possible location types:\n"+listable_location_definitions), response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses}) @api_etag(base_mapdata=True) def location_list(request, filters: Query[LocationListFilters]): @@ -77,7 +79,8 @@ def location_list(request, filters: Query[LocationListFilters]): @map_api_router.get('/locations/full/', summary="list locations (full)", - description="Get locations (with all attributes set)", + description=("Get locations (with all attributes set)\n\n" + "Possible location types:\n"+listable_location_definitions), response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses}) @api_etag(base_mapdata=True) def location_list_full(request, filters: Query[LocationListFilters]): @@ -150,7 +153,8 @@ class ShowRedirects(Schema): @map_api_router.get('/locations/{location_id}/', summary="location by ID (slim)", - description="Get locations by ID (with all attributes set)", + description=("Get locations by ID (with all attributes set)\n\n" + "Possible location types:\n"+all_location_definitions), response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses}) @api_stats('location_get') @api_etag(base_mapdata=True) @@ -164,7 +168,8 @@ def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeo @map_api_router.get('/locations/{location_id}/full/', summary="location by ID (full)", - description="Get location by ID (with all attributes set)", + description=("Get location by ID (with all attributes set)\n\n" + "Possible location types:\n"+all_location_definitions), response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses}) @api_stats('location_get') @api_etag(base_mapdata=True) @@ -202,7 +207,8 @@ def location_by_id_geometry(request, location_id: AnyLocationID): @map_api_router.get('/locations/by-slug/{location_slug}/', summary="location by slug (slim)", - description="Get location by slug (with most important attributes set)", + description=("Get location by slug (with most important attributes set)\n\n" + "Possible location types:\n"+all_location_definitions), response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses}) @api_stats('location_get') @api_etag(base_mapdata=True) @@ -216,7 +222,8 @@ def location_by_slug(request, location_slug: NonEmptyStr, filters: Query[RemoveG @map_api_router.get('/locations/by-slug/{location_slug}/full/', summary="location by slug (full)", - description="Get location by slug (with all attributes set)", + description=("Get location by slug (with all attributes set)\n\n" + "Possible location types:\n"+all_location_definitions), response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses}) @api_stats('location_get') @api_etag(base_mapdata=True) diff --git a/src/c3nav/mapdata/api/mapdata.py b/src/c3nav/mapdata/api/mapdata.py index cd0fe2ef..09d14bcb 100644 --- a/src/c3nav/mapdata/api/mapdata.py +++ b/src/c3nav/mapdata/api/mapdata.py @@ -15,6 +15,7 @@ from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, from c3nav.mapdata.models.locations import DynamicLocation from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema, LevelGeometryFilter, SpaceGeometryFilter) +from c3nav.mapdata.schemas.model_base import schema_description from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema, BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema, DynamicLocationSchema, HoleSchema, LeaveDescriptionSchema, LevelSchema, @@ -58,10 +59,6 @@ def mapdata_retrieve_endpoint(request, model: Type[Model], **lookups): raise API404("%s not found" % model.__name__.lower()) -def schema_description(schema): - return schema.__doc__.replace("\n ", "\n") - - """ Levels """ diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index aea232c4..dc7d4f3a 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -1,4 +1,5 @@ -from typing import Annotated, Any, Optional, Union +import re +from typing import Annotated, Any, Union from ninja import Schema from pydantic import Field as APIField @@ -10,6 +11,31 @@ from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema from c3nav.api.utils import NonEmptyStr +def schema_description(schema): + return schema.__doc__.replace("\n ", "\n").strip() + + +def schema_definition(schema): + return ("- **"+re.sub(r"([a-z])([A-Z])", r"\1 \2", schema.__name__.removesuffix("Schema")) +"**: " + + schema_description(schema).split("\n")[0].strip()) + + +def schema_definitions(schemas): + return "\n".join(schema_definition(schema) for schema in schemas) + + +BoundsSchema = tuple[ + Annotated[tuple[ + Annotated[float, APIField(title="left", description="lowest X coordindate")], + Annotated[float, APIField(title="bottom", description="lowest Y coordindate")] + ], APIField(title="(left, bottom)", description="lowest coordinates", example=(-10, -20))], + Annotated[tuple[ + Annotated[float, APIField(title="right", description="highest X coordindate")], + Annotated[float, APIField(title="top", description="highest Y coordindate")] + ], APIField(title="(right, top)", description="highest coordinates", example=(20, 30))] +] + + class SerializableSchema(Schema): @model_validator(mode="wrap") # noqa @classmethod @@ -23,62 +49,79 @@ class SerializableSchema(Schema): class DjangoModelSchema(SerializableSchema): id: PositiveInt = APIField( title="ID", + example=1, ) class LocationSlugSchema(Schema): slug: NonEmptyStr = APIField( title="location slug", - description="a slug is a unique way to refer to a location across all location types. " - "locations can have a human-readable slug. " - "if it doesn't, this field holds a slug generated based from the location type and ID. " - "this slug will work even if a human-readable slug is defined later. " - "even dynamic locations like coordinates have a slug.", + description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs" + "are meants to be human-readable and easy to remember.\n\n" + "if a location doesn't have a slug defined, this field holds a slug generated from the " + "location type and ID, which will work even if a human-readable slug is defined later.\n\n" + "even dynamic locations like coordinates have an (auto-generated) slug.", + example="entrance", ) class WithAccessRestrictionSchema(Schema): - access_restriction: Optional[PositiveInt] = APIField( + access_restriction: Union[ + Annotated[PositiveInt, APIField(title="access restriction ID")], + Annotated[None, APIField(title="null", description="no access restriction")], + ] = APIField( default=None, title="access restriction ID", + description="access restriction that this object belongs to", ) class TitledSchema(Schema): titles: dict[NonEmptyStr, NonEmptyStr] = APIField( title="title (all languages)", - description="property names are the ISO-language code. languages may be missing.", + description="title in all available languages. property names are the ISO-language code. " + "languages may be missing.", example={ - "en": "Title", - "de": "Titel", + "en": "Entrance", + "de": "Eingang", } ) title: NonEmptyStr = APIField( title="title (preferred language)", - description="preferred language based on the Accept-Language header." + description="title in the preferred language based on the Accept-Language header.", + example="Entrance", ) class LocationSchema(WithAccessRestrictionSchema, TitledSchema, LocationSlugSchema): subtitle: NonEmptyStr = APIField( title="subtitle (preferred language)", - description="an automatically generated short description for this location. " - "preferred language based on the Accept-Language header." + description="an automatically generated short description for this location in the " + "preferred language based on the Accept-Language header.", + example="near Area 51", ) - icon: Optional[NonEmptyStr] = APIField( - default=None, + icon: NonEmptyStr = APIField( title="icon name", - description="any material design icon name" + description="any material design icon name", + example="pin_drop", ) can_search: bool = APIField( title="can be searched", + description="if `true`, this object can show up in search results", ) can_describe: bool = APIField( title="can describe locations", + description="if `true`, this object can be used to describe other locations (e.g. in their subtitle)", ) - add_search: Optional[str] = APIField( + add_search: Union[ + Annotated[str, APIField(title="search terms", description="set when looking for searchable locations")], + Annotated[None, APIField(title="null", description="when not looking for searchable locations")], + ] = APIField( None, - title="more data for search index, only set when looking for searchable locations" + title="additional search terms", + description="more data for the search index separated by spaces, " + "only set when looking for searchable locations", + example="more search terms", ) @@ -90,49 +133,91 @@ class LabelSettingsSchema(DjangoModelSchema): # todo: add titles back in here min_zoom: float = APIField( -10, title="min zoom", + description="minimum zoom to display the label at", ) max_zoom: float = APIField( 10, title="max zoom", + description="maximum, zoom to display the label at", ) font_size: PositiveInt = APIField( title="font size", + description="font size of the label", + example=12, ) class SpecificLocationSchema(LocationSchema): - grid_square: Optional[NonEmptyStr] = APIField( + grid_square: Union[ + Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")], + Annotated[None, APIField(title="null", description="no grid defined or outside of grid")], + ] = APIField( default=None, title="grid square", - description="if a grid is defined and this location is within it", + description="grid cell(s) that this location is in, if a grid is defined and the location is within it", + example="C3", ) - groups: dict[NonEmptyStr, list[PositiveInt] | Optional[PositiveInt]] = APIField( + groups: dict[ + Annotated[NonEmptyStr, APIField(title="location group category name")], + Union[ + Annotated[list[PositiveInt], APIField( + title="array of location IDs", + description="for categories that have `single` set to `false`. can be an empty array.", + example=[1,4,5], + )], + Annotated[PositiveInt, APIField( + title="one location ID", + description="for categories that have `single` set to `true`.", + example=1, + )], + Annotated[None, APIField( + title="null", + description="for categories that have `single` set to `true`." + )], + ] + ] = APIField( title="location groups", - description="grouped by location group categories. " - "property names are the names of location groupes. " - "property values are integer, None or a list of integers, see example." - "see location group category endpoint for currently available possibilities." + description="location group(s) that this specific location belongs to, grouped by categories.\n\n" + "keys are location group category names. see location group category endpoint for details.\n\n" "categories may be missing if no groups apply.", example={ "category_with_single_true": 5, "other_category_with_single_true": None, - "categoryother_category_with_single_false": [1, 3, 7], + "category_with_single_false": [1, 3, 7], } ) - label_settings: Optional[LabelSettingsSchema] = APIField( + label_settings: Union[ + Annotated[LabelSettingsSchema, APIField( + title="label settings", + description="label settings to use", + )], + Annotated[None, APIField( + title="null", + description="label settings from location group will be used, if available" + )], + ] = APIField( default=None, title="label settings", - description="if not set, it may be taken from location groups" + description=( + schema_description(LabelSettingsSchema) + + "\n\nif not set, label settings of location groups might be used" + ) ) - label_override: Optional[NonEmptyStr] = APIField( + label_override: Union[ + Annotated[NonEmptyStr, APIField(title="label override", description="text to use for label")], + Annotated[None, APIField(title="null", description="title will be used")], + ] = APIField( default=None, title="label override (preferred language)", - description="preferred language based on the Accept-Language header." + description="text to use for the label. by default (null), the title would be used." ) class WithPolygonGeometrySchema(Schema): - geometry: Optional[PolygonSchema] = APIField( + geometry: Union[ + PolygonSchema, + Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] + ] = APIField( None, title="geometry", description="can be null if not available or excluded from endpoint", @@ -140,7 +225,10 @@ class WithPolygonGeometrySchema(Schema): class WithLineStringGeometrySchema(Schema): - geometry: Optional[LineStringSchema] = APIField( + geometry: Union[ + LineStringSchema, + Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] + ] = APIField( None, title="geometry", description="can be null if not available or excluded from endpoint", @@ -148,7 +236,10 @@ class WithLineStringGeometrySchema(Schema): class WithPointGeometrySchema(Schema): - geometry: Optional[PointSchema] = APIField( + geometry: Union[ + PointSchema, + Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] + ] = APIField( None, title="geometry", description="can be null if not available or excluded from endpoint", @@ -159,6 +250,7 @@ class WithLevelSchema(SerializableSchema): level: PositiveInt = APIField( title="level", description="level id this object belongs to.", + example=1, ) @@ -166,6 +258,7 @@ class WithSpaceSchema(SerializableSchema): space: PositiveInt = APIField( title="space", description="space id this object belongs to.", + example=1, ) @@ -182,8 +275,8 @@ class SimpleGeometryPointSchema(Schema): class SimpleGeometryPointAndBoundsSchema(SimpleGeometryPointSchema): - bounds: tuple[tuple[float, float], tuple[float, float]] = APIField( - description="location bounding box from (x, y) to (x, y)", + bounds: BoundsSchema = APIField( + description="location bounding box", example=((-10, -20), (20, 30)), ) @@ -212,7 +305,7 @@ Coordinates3D = tuple[float, float, float] AnyLocationID = Union[ Annotated[PositiveInt, APIField( title="location ID", - description="numeric ID of any lcation" + description="numeric ID of any lcation – all locations have a shared ID space" )], CustomLocationID, PositionID, diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index d0d8f1e4..fbfa3813 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -13,7 +13,8 @@ from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, Cust SimpleGeometryPointSchema, SpecificLocationSchema, TitledSchema, WithAccessRestrictionSchema, WithLevelSchema, WithLineStringGeometrySchema, WithPointGeometrySchema, - WithPolygonGeometrySchema, WithSpaceSchema) + WithPolygonGeometrySchema, WithSpaceSchema, schema_definitions, + schema_description) class LevelSchema(SpecificLocationSchema, DjangoModelSchema): @@ -26,18 +27,26 @@ class LevelSchema(SpecificLocationSchema, DjangoModelSchema): title="short label (for level selector)", description="unique among levels", ) - on_top_of: Optional[PositiveInt] = APIField( + on_top_of: Union[ + Annotated[PositiveInt, APIField(title="level ID", description="level this level is on top of", example=1)], + Annotated[None, APIField(title="null", description="this is a main level, not on top of any other")] + ] = APIField( title="on top of level ID", description="if set, this is not a main level, but it's on top of this other level" ) base_altitude: float = APIField( title="base/default altitude", + description="default ground altitude for this level, if it can't be determined using altitude markers.", ) default_height: PositiveFloat = APIField( title="default ceiling height", + description="default ceiling height for all spaces that don't set their own", + example=2.5 ) door_height: PositiveFloat = APIField( title="door height", + description="height for all doors on this level", + example="2.0", ) @@ -86,7 +95,8 @@ class AreaSchema(WithPolygonGeometrySchema, SpecificLocationSchema, WithSpaceSch """ slow_down_factor: PositiveFloat = APIField( title="slow-down factor", - description="how much walking in this area is slowed down, overlapping areas are multiplied" + description="how much walking in this area is slowed down, overlapping areas are multiplied", + example=1.0, ) @@ -212,16 +222,31 @@ class LocationGroupSchema(LocationSchema, DjangoModelSchema): ) priority: int = APIField() # todo: ??? hierarchy: int = APIField() # todo: ??? - label_settings: Optional[LabelSettingsSchema] = APIField( + label_settings: Union[ + Annotated[LabelSettingsSchema, APIField( + title="label settings", + description="label settings to use for gruop members that don't have their own set", + )], + Annotated[None, APIField( + title="null", + description="no label settings set" + )], + ] = APIField( default=None, title="label settings", - description="for locations with this group, can be overwritten by specific locations" + description=( + schema_description(LabelSettingsSchema) + + "\n\nlocations can override this setting" + ) ) can_report_missing: bool = APIField( title="report missing locations", description="can be used in form for reporting missing locations", ) - color: Optional[NonEmptyStr] = APIField( + color: Union[ + Annotated[NonEmptyStr, APIField(title="color", description="a valid CSS color expression")], + Annotated[None, APIField(title="null", description="default/no color will be used")], + ] = APIField( title="color", description="an optional color for spaces and areas with this group" ) @@ -285,7 +310,7 @@ class LocationGroupCategorySchema(TitledSchema, DjangoModelSchema): class DynamicLocationSchema(SpecificLocationSchema, DjangoModelSchema): """ - A dynamic location represents a moving object. Its position has to be separately queries through the position API. + Represents a moving object. Its position has to be separately queried through the position API. A dynamic location is a specific location, and can therefore be routed to and from, as well as belong to location groups. @@ -344,10 +369,10 @@ class CustomLocationSchema(SerializableSchema): slug: CustomLocationID = APIField( description="slug, identical to ID" ) - icon: Optional[NonEmptyStr] = APIField( - default=None, + icon: NonEmptyStr = APIField( title="icon name", - description="any material design icon name" + description="any material design icon name", + example="pin_drop", ) title: NonEmptyStr = APIField( title="title (preferred language)", @@ -359,32 +384,56 @@ class CustomLocationSchema(SerializableSchema): "preferred language based on the Accept-Language header." ) level: PositiveInt = APIField( - description="level ID this custom location is located on" + description="level ID this custom location is located on", + example=1, ) - space: Optional[PositiveInt] = APIField( - description="space ID this custom location is located in, if applicable" + space: Union[ + Annotated[PositiveInt, APIField(title="space ID", example=1)], + Annotated[None, APIField(title="null", description="the location is not inside a space")], + ] = APIField( + default=None, + description="space ID this custom location is located in" ) areas: list[PositiveInt] = APIField( description="IDs of areas this custom location is located in" ) - grid_square: Optional[NonEmptyStr] = APIField( + grid_square: Union[ + Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")], + Annotated[None, APIField(title="null", description="no grid defined or outside of grid")], + ] = APIField( default=None, title="grid square", - description="if a grid is defined and this custom location is within it", + description="grid cell(s) that this location is in, if a grid is defined and the location is within it", + example="C3", ) - near_area: Optional[PositiveInt] = APIField( - description="the ID of an area near this custom location, if there is one" + near_area: Union[ + Annotated[PositiveInt, APIField(title="area ID", example=1)], + Annotated[None, APIField(title="null", description="the location is not near any areas")], + ] = APIField( + near="nearby area", + description="the ID of an area near this custom location" ) - near_poi: Optional[PositiveInt] = APIField( - description="the ID of a POI near this custom location, if there is one" + near_poi: Union[ + Annotated[PositiveInt, APIField(title="POI ID", example=1)], + Annotated[None, APIField(title="null", description="the location is not near any POIs")], + ] = APIField( + title="nearby POI", + description="the ID of a POI near this custom location" ) nearby: list[PositiveInt] = APIField( description="list of IDs of nearby locations" ) - altitude: Optional[float] = APIField( - description="ground altitude (in the map-wide coordinate system), if it can be determined" + altitude: Union[ + Annotated[float, APIField(title="ground altitude", example=1)], + Annotated[None, APIField(title="null", description="could not be determined (outside of space?)")], + ] = APIField( + title="ground altitude", + description="ground altitude (in the map-wide coordinate system)" ) - geometry: Optional[PointSchema] = APIField( + geometry: Union[ + PointSchema, + Annotated[None, APIField(title="null", description="geometry excluded from endpoint")] + ] = APIField( None, description="point geometry for this custom location", ) @@ -392,37 +441,43 @@ class CustomLocationSchema(SerializableSchema): class TrackablePositionSchema(Schema): """ - A trackable position. It's position can be set or reset. + A trackable position. Its position can be set or reset. """ id: PositionID = APIField( - description="ID representing the position" + description="ID representing the position", + example="p:adskjfalskdj", ) slug: PositionID = APIField( - description="slug representing the position" + description="slug representing the position", + example="p:adskjfalskdj", ) - icon: Optional[NonEmptyStr] = APIField( - default=None, + icon: NonEmptyStr = APIField( title="icon name", - description="any material design icon name" + description="any material design icon name", + example="pin_drop", ) title: NonEmptyStr = APIField( title="title of the position", + example="My position" ) subtitle: NonEmptyStr = APIField( title="subtitle (preferred language)", description="an automatically generated short description, which might change when the position changes. " - "preferred language based on the Accept-Language header." + "preferred language based on the Accept-Language header.", + example="Near Bällebad" ) -def put_locationtype_first(schema): - fields = schema.__fields__.copy() - schema.__fields__ = {"locationtype": fields.pop("locationtype"), **fields} - return schema - - class LocationTypeSchema(Schema): - locationtype: str + locationtype: str = APIField(title="location type", + description="indicates what kind of location is included. " + "different location types have different fields.") + + +def LocationTypeAPIField(): + return APIField(title="location type", + description="indicates what kind of location is included. " + "different location types have different fields.") class FullLevelLocationSchema(LevelSchema, LocationTypeSchema): @@ -430,7 +485,7 @@ class FullLevelLocationSchema(LevelSchema, LocationTypeSchema): A level for the location API. See Level schema for details. """ - locationtype: Literal["level"] + locationtype: Literal["level"] = LocationTypeAPIField() class FullSpaceLocationSchema(SimpleGeometryPointAndBoundsSchema, SpaceSchema, LocationTypeSchema): @@ -438,7 +493,7 @@ class FullSpaceLocationSchema(SimpleGeometryPointAndBoundsSchema, SpaceSchema, L A space with some additional information for the location API. See Space schema for details. """ - locationtype: Literal["space"] + locationtype: Literal["space"] = LocationTypeAPIField() class FullAreaLocationSchema(SimpleGeometryPointAndBoundsSchema, AreaSchema, LocationTypeSchema): @@ -446,7 +501,7 @@ class FullAreaLocationSchema(SimpleGeometryPointAndBoundsSchema, AreaSchema, Loc An area with some additional information for the location API. See Area schema for details. """ - locationtype: Literal["area"] + locationtype: Literal["area"] = LocationTypeAPIField() class FullPOILocationSchema(SimpleGeometryPointSchema, POISchema, LocationTypeSchema): @@ -454,7 +509,7 @@ class FullPOILocationSchema(SimpleGeometryPointSchema, POISchema, LocationTypeSc A point of interest with some additional information for the location API. See POI schema for details. """ - locationtype: Literal["poi"] + locationtype: Literal["poi"] = LocationTypeAPIField() class FullLocationGroupLocationSchema(SimpleGeometryLocationsSchema, LocationGroupSchema, LocationTypeSchema): @@ -462,7 +517,7 @@ class FullLocationGroupLocationSchema(SimpleGeometryLocationsSchema, LocationGro A location group with some additional information for the location API. See LocationGroup schema for details. """ - locationtype: Literal["locationgroup"] + locationtype: Literal["locationgroup"] = LocationTypeAPIField() class FullDynamicLocationLocationSchema(DynamicLocationSchema, LocationTypeSchema): @@ -470,7 +525,7 @@ class FullDynamicLocationLocationSchema(DynamicLocationSchema, LocationTypeSchem A dynamic location for the location API. See DynamicLocation schema for details. """ - locationtype: Literal["dynamiclocation"] + locationtype: Literal["dynamiclocation"] = LocationTypeAPIField() class CustomLocationLocationSchema(SimpleGeometryPointAndBoundsSchema, CustomLocationSchema, LocationTypeSchema): @@ -478,7 +533,7 @@ class CustomLocationLocationSchema(SimpleGeometryPointAndBoundsSchema, CustomLoc A custom location for the location API. See CustomLocation schema for details. """ - locationtype: Literal["customlocation"] + locationtype: Literal["customlocation"] = LocationTypeAPIField() class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchema): @@ -486,7 +541,7 @@ class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchem A trackable position for the location API. See TrackablePosition schema for details. """ - locationtype: Literal["position"] + locationtype: Literal["position"] = LocationTypeAPIField() class SlimLocationMixin(Schema): @@ -599,6 +654,14 @@ SlimLocationSchema = Annotated[ ] +listable_location_definitions = schema_definitions( + (LevelSchema, SpaceSchema, AreaSchema, POISchema, DynamicLocationSchema, LocationGroupSchema) +) +all_location_definitions = listable_location_definitions + "\n" + schema_definitions( + (CustomLocationSchema, TrackablePositionSchema) +) + + class DisplayLink(Schema): """ A link for the location display @@ -612,14 +675,24 @@ class DisplayLink(Schema): class LocationDisplay(SerializableSchema): id: AnyLocationID = APIField( description="a numeric ID for a map location or a string ID for generated locations", + example=1, ) - level: Optional[PositiveInt] = APIField( + + level: Union[ + Annotated[PositiveInt, APIField(title="level ID", description="ID of relevant level")], + Annotated[None, APIField(title="null", description="no relevant level")], + ] = APIField( None, - description="level ID, if applicable" + title="level", + example=2, ) - space: Optional[PositiveInt] = APIField( + space: Union[ + Annotated[PositiveInt, APIField(title="level ID", description="ID of relevant level")], + Annotated[None, APIField(title="null", description="no relevant level")], + ] = APIField( None, - description="space ID, if applicable" + description="space", + example=3, ) display: list[ tuple[ @@ -631,12 +704,48 @@ class LocationDisplay(SerializableSchema): Annotated[None, APIField(title="no value")] ], APIField(title="field value", union_mode='left_to_right')] ] - ] = APIField(description="a list of human-readable display values") - geometry: Optional[GeometrySchema] = APIField( + ] = APIField( + title="display fields", + description="a list of human-readable display values", + example=[ + ("Title", "Awesome location"), + ("Access Restriction", None), + ("Level", { + "id": 2, + "slug": "level0", + "title": "Ground Floor", + "can_search": True, + }), + ("Groups", [ + { + "id": 10, + "slug": "entrances", + "title": "Entrances", + "can_search": True, + }, + { + "id": 11, + "slug": "startswithe", + "title": "Locations that Start with E", + "can_search": False, + } + ]) + ] + ) + geometry: Union[ + GeometrySchema, + Annotated[None, APIField(title="null", description="no geometry available")] + ] = APIField( None, description="representative geometry, if available" ) - editor_url: Optional[NonEmptyStr] = APIField( - None, description="path to edit this object in the editor, if the user has access to it", + editor_url: Union[ + Annotated[NonEmptyStr, APIField(title="path to editor")], + Annotated[None, APIField(title="null", description="no editor access or object is not editable")], + ] = APIField( + None, + title="editor URL", + description="path to edit this object in the editor", + example="/editor/spaces/2/pois/1/" ) diff --git a/src/c3nav/mapdata/schemas/responses.py b/src/c3nav/mapdata/schemas/responses.py index 0b94ac34..099ead74 100644 --- a/src/c3nav/mapdata/schemas/responses.py +++ b/src/c3nav/mapdata/schemas/responses.py @@ -1,27 +1,18 @@ -from typing import Annotated, Optional +from typing import Annotated, Union from ninja import Schema from pydantic import Field as APIField from pydantic import PositiveInt from c3nav.api.schema import GeometrySchema -from c3nav.mapdata.schemas.model_base import AnyLocationID +from c3nav.mapdata.schemas.model_base import AnyLocationID, BoundsSchema -class BoundsSchema(Schema): +class WithBoundsSchema(Schema): """ Describing a bounding box """ - bounds: tuple[ - Annotated[tuple[ - Annotated[float, APIField(title="left", description="lowest X coordindate")], - Annotated[float, APIField(title="bottom", description="lowest Y coordindate")] - ], APIField(title="(left, bottom)", description="lowest coordinates", example=(-10, -20))], - Annotated[tuple[ - Annotated[float, APIField(title="right", description="highest X coordindate")], - Annotated[float, APIField(title="top", description="highest Y coordindate")] - ], APIField(title="(right, top)", description="highest coordinates", example=(20, 30))] - ] = APIField( + bounds: BoundsSchema = APIField( title="boundaries", description="(left, bottom) to (top, right)", ) @@ -31,9 +22,15 @@ class LocationGeometry(Schema): id: AnyLocationID = APIField( description="ID of the location that the geometry is being queried for", ) - level: Optional[PositiveInt] = APIField( + level: Union[ + Annotated[PositiveInt, APIField(title="level ID")], + Annotated[None, APIField(title="null", description="geometry is not on any level")], # todo: possible? + ] = APIField( description="ID of the level the geometry is on", ) - geometry: Optional[GeometrySchema] = APIField( + geometry: Union[ + GeometrySchema, + Annotated[None, APIField(title="null", description="no geometry available")] + ] = APIField( description="geometry, if available" )