diff --git a/apps/transport/lib/transport_web/api/controllers/aom_controller.ex b/apps/transport/lib/transport_web/api/controllers/aom_controller.ex index 68cf642ea9..8d8113f0fd 100644 --- a/apps/transport/lib/transport_web/api/controllers/aom_controller.ex +++ b/apps/transport/lib/transport_web/api/controllers/aom_controller.ex @@ -16,8 +16,7 @@ defmodule TransportWeb.API.AomController do def by_coordinates_operation, do: %Operation{ tags: ["aom"], - summary: "Show AOM by coordinates", - description: "Show covered regions", + summary: "Show first AOM containing a point with given coordinates", operationId: "API.AOMController.by_coordinates", parameters: [ Operation.parameter(:lon, :query, :number, "Longitude"), @@ -43,9 +42,8 @@ defmodule TransportWeb.API.AomController do @spec by_insee_operation :: OpenApiSpex.Operation.t() def by_insee_operation, do: %Operation{ - tags: ["insee"], - summary: "Show AOM by INSEE", - description: "Show covered regions", + tags: ["aom"], + summary: "Search AOM by INSEE code", operationId: "API.AOMController.by_insee_operation", parameters: [ Operation.parameter(:insee, :path, :string, "INSEE") @@ -92,9 +90,8 @@ defmodule TransportWeb.API.AomController do @spec geojson_operation :: OpenApiSpex.Operation.t() def geojson_operation, do: %Operation{ - tags: ["geojson"], - summary: "Show geojson of AOM", - description: "Show covered regions", + tags: ["aom"], + summary: "Show GeoJSON of all available AOMs (Autorités Organisatrices de la Mobilité)", operationId: "API.AOMController.geojson_operation", parameters: [], responses: %{ diff --git a/apps/transport/lib/transport_web/api/controllers/datasets_controller.ex b/apps/transport/lib/transport_web/api/controllers/datasets_controller.ex index eccc6f4953..926de2f8e5 100644 --- a/apps/transport/lib/transport_web/api/controllers/datasets_controller.ex +++ b/apps/transport/lib/transport_web/api/controllers/datasets_controller.ex @@ -4,7 +4,7 @@ defmodule TransportWeb.API.DatasetController do alias Helpers alias OpenApiSpex.Operation alias DB.{AOM, Dataset, Repo, Resource} - alias TransportWeb.API.Schemas.{DatasetsResponse, GeoJSONResponse} + alias TransportWeb.API.Schemas.{DatasetDetails, DatasetsResponse, GeoJSONResponse} alias Geo.{JSON, MultiPolygon} # The default (one minute) felt a bit too high for someone doing scripted operations @@ -19,12 +19,15 @@ defmodule TransportWeb.API.DatasetController do def datasets_operation, do: %Operation{ tags: ["datasets"], - summary: "Show datasets and its resources", - description: "For every dataset, show its associated resources, url and validity date", + summary: "List all datasets (with their resources)", + description: ~s"This call returns (in a single, non-paginated response) the list of all the + datasets referenced on the site, along with their associated resources. The datasets + and resources are here provided in summarized form (without history & conversions). + You can call `/api/datasets/:id` for each dataset to get extra data (history & conversions)", operationId: "API.DatasetController.datasets", parameters: [], responses: %{ - 200 => Operation.response("Dataset", "application/json", DatasetsResponse) + 200 => Operation.response("DatasetsResponse", "application/json", DatasetsResponse) } } @@ -50,12 +53,13 @@ defmodule TransportWeb.API.DatasetController do def by_id_operation, do: %Operation{ tags: ["datasets"], - summary: "Show given dataset and its resources", - description: "For one dataset, show its associated resources, url and validity date", + summary: "Return the details of a given dataset and its resources", + description: + ~s"Returns the detailed version of a dataset, showing its resources, the resources history & conversions.", operationId: "API.DatasetController.datasets_by_id", - parameters: [Operation.parameter(:id, :path, :string, "id")], + parameters: [Operation.parameter(:id, :path, :string, "datagouv id of the dataset you want to retrieve")], responses: %{ - 200 => Operation.response("Dataset", "application/json", DatasetsResponse) + 200 => Operation.response("DatasetDetails", "application/json", DatasetDetails) } } @@ -63,8 +67,8 @@ defmodule TransportWeb.API.DatasetController do def geojson_by_id_operation, do: %Operation{ tags: ["datasets"], - summary: "Show given dataset geojson", - description: "For one dataset, show its associated geojson", + summary: "Show given dataset GeoJSON", + description: "For one dataset, show its associated GeoJSON.", operationId: "API.DatasetController.datasets_geojson_by_id", parameters: [Operation.parameter(:id, :path, :string, "id")], responses: %{ @@ -195,6 +199,7 @@ defmodule TransportWeb.API.DatasetController do ) end + # NOTE: only added in detailed dataset view defp add_conversions(%{"resources" => resources} = data, %Dataset{} = dataset) do conversions = dataset diff --git a/apps/transport/lib/transport_web/api/controllers/places_controller.ex b/apps/transport/lib/transport_web/api/controllers/places_controller.ex index 86e486b132..74fbdecfe0 100644 --- a/apps/transport/lib/transport_web/api/controllers/places_controller.ex +++ b/apps/transport/lib/transport_web/api/controllers/places_controller.ex @@ -79,7 +79,7 @@ defmodule TransportWeb.API.PlacesController do %Operation{ tags: ["datasets"], summary: "Autocomplete search for datasets", - description: "Given a search input, return potentialy corresponding results with the associated url", + description: "Given a search input, return potentially corresponding results with the associated search URL", operationId: "API.DatasetController.datasets_autocomplete", parameters: [Operation.parameter(:q, :query, :string, "query")], responses: %{ diff --git a/apps/transport/lib/transport_web/api/controllers/stats_controller.ex b/apps/transport/lib/transport_web/api/controllers/stats_controller.ex index 71cfecdd95..c9d999b6e5 100644 --- a/apps/transport/lib/transport_web/api/controllers/stats_controller.ex +++ b/apps/transport/lib/transport_web/api/controllers/stats_controller.ex @@ -13,7 +13,7 @@ defmodule TransportWeb.API.StatsController do @spec regions_operation() :: Operation.t() def regions_operation, do: %Operation{ - tags: ["regions"], + tags: ["stats"], summary: "Show regions", description: "Show covered french administrative regions", operationId: "API.StatsController.regions", @@ -26,7 +26,7 @@ defmodule TransportWeb.API.StatsController do @spec index_operation() :: Operation.t() def index_operation, do: %Operation{ - tags: ["index"], + tags: ["stats"], summary: "Show regions", description: "Show covered french administrative regions", operationId: "API.StatsController.index", @@ -39,7 +39,7 @@ defmodule TransportWeb.API.StatsController do @spec bike_scooter_sharing_operation() :: Operation.t() def bike_scooter_sharing_operation, do: %Operation{ - tags: ["bike-scooter-sharing"], + tags: ["stats"], summary: "Show bike and scooter sharing stats", description: "Show bike and scooter sharing stats", operationId: "API.StatsController.bike_scooter_sharing", @@ -52,7 +52,7 @@ defmodule TransportWeb.API.StatsController do @spec quality_operation() :: Operation.t() def quality_operation, do: %Operation{ - tags: ["quality"], + tags: ["stats"], summary: "Show data quality stats", description: "Show data quality stats", operationId: "API.StatsController.quality", @@ -66,6 +66,7 @@ defmodule TransportWeb.API.StatsController do def geojson(features), do: %{ "type" => "FeatureCollection", + # This is now completely incorrect! "name" => "Autorités organisatrices de Mobiltés", "features" => features } @@ -376,6 +377,9 @@ defmodule TransportWeb.API.StatsController do %{ "geometry" => r.geometry |> JSON.encode!(), "type" => "Feature", + # NOTE: there is a bug here - the key is an atom. + # I won't change it now because it would mean more changes somewhere else, maybe. + # `Map.reject(fn({k,v}) -> k == :geometry end)` will do it. "properties" => Map.take(r, Enum.filter(Map.keys(r), fn k -> k != "geometry" end)) } end) diff --git a/apps/transport/lib/transport_web/api/router.ex b/apps/transport/lib/transport_web/api/router.ex index 3bc4a62469..89abf5dd23 100644 --- a/apps/transport/lib/transport_web/api/router.ex +++ b/apps/transport/lib/transport_web/api/router.ex @@ -60,7 +60,7 @@ defmodule TransportWeb.API.Router do do: %{ info: %{ version: "1.0", - title: "Transport.data.gouv.fr API" + title: "transport.data.gouv.fr API" } } end diff --git a/apps/transport/lib/transport_web/api/schemas.ex b/apps/transport/lib/transport_web/api/schemas.ex index 33babb5fee..15c40250d7 100644 --- a/apps/transport/lib/transport_web/api/schemas.ex +++ b/apps/transport/lib/transport_web/api/schemas.ex @@ -1,6 +1,14 @@ defmodule TransportWeb.API.Schemas do @moduledoc """ OpenAPI schema defintions + + Useful documentation: + - https://json-schema.org/understanding-json-schema/reference/array.html + - https://json-schema.org/understanding-json-schema/reference/object.html + - https://json-schema.org/understanding-json-schema/reference/string.html + + A good chunk of our GeoJSON responses do not pass our OpenAPI specs. It would need more work. + """ require OpenApiSpex alias OpenApiSpex.{ExternalDocumentation, Schema} @@ -14,13 +22,24 @@ defmodule TransportWeb.API.Schemas do type: :object, description: "GeoJSon geometry", required: [:type], - externalDocs: %ExternalDocumentation{url: "http://geojson.org/geojson-spec.html#geometry-objects"}, + externalDocs: %ExternalDocumentation{ + url: "http://geojson.org/geojson-spec.html#geometry-objects" + }, properties: %{ type: %Schema{ type: :string, - enum: ["Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon"] + enum: [ + "Point", + "LineString", + "Polygon", + "MultiPoint", + "MultiLineString", + "MultiPolygon" + ] } - } + }, + # allow extra properties since this is used as a composable base + additionalProperties: true }) end @@ -66,7 +85,8 @@ defmodule TransportWeb.API.Schemas do coordinates: %Schema{type: :array, items: Point2D} } } - ] + ], + additionalProperties: false }) end @@ -77,17 +97,27 @@ defmodule TransportWeb.API.Schemas do OpenApiSpex.schema(%{ type: :object, title: "Polygon", - description: "GeoJSon geometry", + description: "GeoJSON geometry", externalDocs: %ExternalDocumentation{url: "http://geojson.org/geojson-spec.html#id4"}, allOf: [ GeometryBase.schema(), %Schema{ type: :object, properties: %{ - coordinates: %Schema{type: :array, items: %Schema{type: :array, items: Point2D}} + coordinates: %Schema{ + type: :array, + items: %Schema{ + type: :array, + items: %Schema{ + type: :array, + items: Point2D + } + } + } } } - ] + ], + additionalProperties: false }) end @@ -108,7 +138,8 @@ defmodule TransportWeb.API.Schemas do coordinates: %Schema{type: :array, items: Point2D} } } - ] + ], + additionalProperties: false }) end @@ -129,7 +160,8 @@ defmodule TransportWeb.API.Schemas do coordinates: %Schema{type: :array, items: %Schema{type: :array, items: Point2D}} } } - ] + ], + additionalProperties: false }) end @@ -153,7 +185,8 @@ defmodule TransportWeb.API.Schemas do } } } - ] + ], + additionalProperties: false }) end @@ -171,7 +204,8 @@ defmodule TransportWeb.API.Schemas do MultiPoint.schema(), MultiLineString.schema(), MultiPolygon.schema() - ] + ], + additionalProperties: false }) end @@ -187,11 +221,16 @@ defmodule TransportWeb.API.Schemas do properties: %{ type: %Schema{type: :string, enum: ["Feature"]}, geometry: Geometry, - properties: %Schema{type: :object}, - id: %Schema{ - oneOf: [%Schema{type: :string}, %Schema{type: :number}] + properties: %Schema{ + type: :object, + properties: %{ + id: %Schema{ + oneOf: [%Schema{type: :string}, %Schema{type: :number}] + } + } } - } + }, + additionalProperties: false }) end @@ -204,8 +243,11 @@ defmodule TransportWeb.API.Schemas do title: "FeatureCollection", description: "FeatureCollection object", properties: %{ - features: %Schema{type: :array, items: Feature} - } + features: %Schema{type: :array, items: Feature}, + type: %Schema{type: :string, enum: ["FeatureCollection"], required: true}, + name: %Schema{type: :string} + }, + additionalProperties: false }) end @@ -213,17 +255,176 @@ defmodule TransportWeb.API.Schemas do @moduledoc false require OpenApiSpex + @properties %{ + siren: %Schema{type: :string, nullable: true}, + nom: %Schema{type: :string}, + insee_commune_principale: %Schema{type: :string}, + forme_juridique: %Schema{type: :string}, + departement: %Schema{type: :string} + } + + OpenApiSpex.schema(%{ + title: "AOMResponse", + description: "AOM object, as returned from AOMs endpoints", + type: :object, + properties: @properties, + required: @properties |> Map.keys(), + additionalProperties: false + }) + end + + defmodule AOMShortRef do + @moduledoc false + require OpenApiSpex + OpenApiSpex.schema(%{ - title: "AOM", - description: "AOM object", + title: "AOMShortRef", + description: + "AOM object, as embedded in datasets (short version - DEPRECATED, only there for retrocompatibility, use covered_area instead)", type: :object, + required: [:name], properties: %{ - siren: %Schema{type: :string}, - nom: %Schema{type: :string}, - insee_commune_principale: %Schema{type: :string}, - forme_juridique: %Schema{type: :string}, - departement: %Schema{type: :string} - } + # nullable because we saw it null in actual production data + # probably exactly what's described in https://github.com/etalab/transport-site/issues/3422 + siren: %Schema{type: :string, nullable: true}, + name: %Schema{type: :string, nullable: true} + }, + additionalProperties: false + }) + end + + defmodule CoveredArea.Country do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "CoveredArea.Country", + type: :object, + required: [ + :country, + :name, + :type + ], + properties: %{ + name: %Schema{type: :string}, + type: %Schema{type: :string, enum: ["country"], required: true}, + country: %Schema{ + type: :object, + required: [:name], + properties: %{ + name: %Schema{type: :string} + }, + additionalProperties: false + } + }, + additionalProperties: false + }) + end + + # Very similar to `AOMShortRef` but ultimately only CoveredAreas should be kept (?) + defmodule CoveredArea.AOM do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "CoveredArea.AOM", + type: :object, + required: [ + :aom, + :name, + :type + ], + properties: %{ + name: %Schema{type: :string}, + type: %Schema{type: :string, enum: ["aom"], required: true}, + aom: %Schema{ + type: :object, + required: [:name, :siren], + properties: %{ + name: %Schema{type: :string}, + siren: %Schema{type: :string} + }, + additionalProperties: false + } + }, + additionalProperties: false + }) + end + + defmodule CoveredArea.Cities do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "CoveredArea.Cities", + type: :object, + required: [ + :cities, + :name, + :type + ], + properties: %{ + name: %Schema{type: :string}, + type: %Schema{type: :string, enum: ["cities"], required: true}, + cities: %Schema{ + type: :array, + items: %Schema{ + type: :object, + required: [:name, :insee], + properties: %{ + name: %Schema{type: :string}, + insee: %Schema{type: :string} + }, + additionalProperties: false + } + } + }, + additionalProperties: false + }) + end + + defmodule CoveredArea.Region do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "CoveredArea.Region", + type: :object, + required: [ + :region, + :name, + :type + ], + properties: %{ + name: %Schema{type: :string}, + type: %Schema{type: :string, enum: ["region"], required: true}, + region: %Schema{ + type: :object, + required: [:name], + properties: %{ + name: %Schema{type: :string} + }, + additionalProperties: false + } + }, + additionalProperties: false + }) + end + + defmodule CoveredArea do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "CoveredArea", + type: :object, + oneOf: [ + CoveredArea.Country.schema(), + CoveredArea.AOM.schema(), + CoveredArea.Region.schema(), + CoveredArea.Cities.schema() + ], + additionalProperties: false }) end @@ -239,35 +440,93 @@ defmodule TransportWeb.API.Schemas do Geometry.schema(), Feature.schema(), FeatureCollection.schema() - ] + ], + additionalProperties: false + }) + end + + defmodule Publisher do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Publisher", + description: "Publisher", + type: :object, + properties: %{ + # as seen in production data + name: %Schema{type: :string, nullable: true}, + type: %Schema{type: :string} + }, + additionalProperties: false }) end - defmodule Utils do + defmodule ResourceUtils do @moduledoc false def get_resource_prop(conversions: false), do: %{ - url: %Schema{type: :string, description: "Stable URL of the file"}, - original_url: %Schema{type: :string, description: "Direct URL of the file"}, - title: %Schema{type: :string, description: "Title of the resource"}, - updated: %Schema{type: :string, description: "Last update date-time"}, - end_calendar_validity: %Schema{ + datagouv_id: %Schema{ type: :string, - description: - "The last day of the validity period of the file (read from the calendars for the GTFS). null if the file couldn’t be read" + description: "Data gouv id of the resource" + }, + id: %Schema{ + type: :integer, + description: "transport.data.gouv.fr's ID" + }, + format: %Schema{ + type: :string, + description: "The format of the resource (GTFS, NeTEx, etc.)" + }, + is_available: %Schema{ + type: :boolean, + description: "Availability of the resource" }, - start_calendar_validity: %Schema{ + original_url: %Schema{ type: :string, - description: - "The first day of the validity period of the file (read from the calendars for the GTFS). null if the file couldn’t be read" + description: "Direct URL of the file" + }, + url: %Schema{type: :string, description: "Stable URL of the file"}, + page_url: %Schema{ + type: :string, + description: "URL of the resource on transport.data.gouv.fr" + }, + features: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Features" + }, + title: %Schema{type: :string, description: "Title of the resource"}, + filesize: %Schema{ + type: :integer, + description: "Size of the resource in bytes" }, - format: %Schema{type: :string, description: "The format of the resource (GTFS, NeTEx, etc.)"}, metadata: %Schema{ type: :object, description: "Some metadata about the resource" + }, + type: %Schema{type: :string, description: "Category of the data"}, + modes: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Types of transportation" + }, + updated: %Schema{ + type: :string, + format: "date-time", + description: "Last update date-time" + }, + schema_name: %Schema{ + type: :string, + description: "Data schema followed by the resource" + }, + schema_version: %Schema{ + type: :string, + description: "Version of the data schema followed by the resource" } } + # conversions are only shown in the detailed dataset view def get_resource_prop(conversions: true), do: [conversions: false] @@ -279,16 +538,52 @@ defmodule TransportWeb.API.Schemas do GeoJSON: %Schema{ type: :object, description: "Conversion to the GeoJSON format", - properties: conversion_properties() + required: conversion_properties() |> Map.keys(), + properties: conversion_properties(), + additionalProperties: false }, NeTEx: %Schema{ type: :object, description: "Conversion to the NeTEx format", - properties: conversion_properties() + required: conversion_properties() |> Map.keys(), + properties: conversion_properties(), + additionalProperties: false } } }) + def get_community_resource_prop do + [conversions: false] + |> ResourceUtils.get_resource_prop() + |> Map.put(:community_resource_publisher, %Schema{ + type: :string, + description: "Name of the producer of the community resource" + }) + |> Map.put( + :original_resource_url, + %Schema{ + type: :string, + description: """ + Some community resources have been generated from another dataset (like the generated NeTEx / GeoJSON). + Those resources have a `original_resource_url` equals to the original resource's `original_url` + """ + } + ) + end + + # DRYing keys here + def get_resource_optional_properties_keys do + [ + :features, + :filesize, + :metadata, + :modes, + :original_resource_url, + :schema_name, + :schema_version + ] + end + defp conversion_properties, do: %{ filesize: %Schema{type: :integer, description: "File size in bytes"}, @@ -301,14 +596,35 @@ defmodule TransportWeb.API.Schemas do } end - defmodule Resource do + defmodule DetailedResource do + @moduledoc false + require OpenApiSpex + + @properties ResourceUtils.get_resource_prop(conversions: true) + @optional_properties ResourceUtils.get_resource_optional_properties_keys() + + OpenApiSpex.schema(%Schema{ + type: :object, + description: "A single resource (including conversions)", + required: (@properties |> Map.keys()) -- @optional_properties, + properties: @properties, + additionalProperties: false + }) + end + + defmodule SummarizedResource do @moduledoc false require OpenApiSpex + @properties ResourceUtils.get_resource_prop(conversions: false) + @optional_properties ResourceUtils.get_resource_optional_properties_keys() + OpenApiSpex.schema(%Schema{ type: :object, - description: "A single resource", - properties: Utils.get_resource_prop(conversions: true) + description: "A single resource (summarized version)", + required: (@properties |> Map.keys()) -- @optional_properties, + properties: @properties, + additionalProperties: false }) end @@ -316,51 +632,161 @@ defmodule TransportWeb.API.Schemas do @moduledoc false require OpenApiSpex + @properties ResourceUtils.get_community_resource_prop() + @optional_properties ResourceUtils.get_resource_optional_properties_keys() + OpenApiSpex.schema(%Schema{ type: :object, description: "A single community resource", - properties: - [conversions: false] - |> Utils.get_resource_prop() - |> Map.put(:community_resource_publisher, %Schema{ - type: :string, - description: "Name of the producer of the community resource" - }) - |> Map.put(:original_resource_url, %Schema{ - type: :string, - description: """ - some community resources have been generated from another dataset (like the generated NeTEx / GeoJson). - Those resources have a `original_resource_url` equals to the original resource's `original_url` - """ - }) + required: (@properties |> Map.keys()) -- @optional_properties, + properties: @properties, + additionalProperties: false }) end - defmodule DatasetsResponse do + defmodule ResourceHistory do @moduledoc false require OpenApiSpex - OpenApiSpex.schema(%{ - title: "Dataset", - description: "A dataset is a composed of at least one GTFS resource", + @properties %{ + inserted_at: %Schema{type: :string, format: "date-time"}, + updated_at: %Schema{type: :string, format: "date-time"}, + last_up_to_date_at: %Schema{type: :string, format: "date-time", nullable: true}, + payload: %Schema{type: :object, description: "Payload (loosely specified at the moment)"}, + latest_schema_version_to_date: %Schema{type: :string}, + permanent_url: %Schema{type: :string}, + resource_latest_url: %Schema{type: :string}, + resource_url: %Schema{type: :string}, + # NOTE: apparently, can be nil sometimes! This should be investigated + resource_id: %Schema{type: :integer, nullable: true}, + schema_name: %Schema{type: :string}, + schema_version: %Schema{type: :string}, + title: %Schema{type: :string}, + uuid: %Schema{type: :string} + } + @optional_properties [ + :latest_schema_version_to_date, + :permanent_url, + :resource_latest_url, + :resource_url, + :schema_name, + :schema_version, + :uuid, + :title + ] + + OpenApiSpex.schema(%Schema{ type: :object, - properties: %{ - updated: %Schema{type: :string, description: "The last update of any resource of that dataset"}, - name: %Schema{type: :string}, - licence: %Schema{type: :string, description: "The licence of the dataset"}, - created_at: %Schema{type: :string, format: :date, description: "Date of creation of the dataset"}, - aom: %Schema{type: :string, description: "Transit authority responsible of this authority"}, + description: "A resource version", + required: (@properties |> Map.keys()) -- @optional_properties, + properties: @properties, + additionalProperties: false + }) + end + + defmodule DatasetUtils do + @moduledoc false + + def get_dataset_prop(details: details) do + # base resource comes in 2 flavors + resource_type = if details == true, do: DetailedResource, else: SummarizedResource + + base = %{ + datagouv_id: %Schema{ + type: :string, + description: "Data gouv id for this dataset" + }, + id: %Schema{type: :string, description: "Same as datagouv_id"}, + updated: %Schema{ + type: :string, + format: :"date-time", + description: "The last update of any resource of that dataset (`null` if the dataset has no resources)", + nullable: true + }, + page_url: %Schema{ + type: :string, + description: "transport.data.gouv.fr page for this dataset" + }, + publisher: Publisher.schema(), + slug: %Schema{type: :string, description: "unique dataset slug"}, + title: %Schema{type: :string}, + type: %Schema{type: :string}, + licence: %Schema{ + type: :string, + description: "The licence of the dataset" + }, + created_at: %Schema{ + type: :string, + format: :date, + description: "Date of creation of the dataset" + }, + # Obsolete, to be removed (see https://github.com/etalab/transport-site/issues/3422) + aom: AOMShortRef.schema(), resources: %Schema{ type: :array, - description: "All the resources (files) associated with the dataset", - items: Resource + description: "All the resources associated with the dataset", + # NOTE: community resources will have to be removed from here + # https://github.com/etalab/transport-site/issues/3407 + items: %Schema{anyOf: [resource_type, CommunityResource]} }, community_resources: %Schema{ type: :array, - description: "All the community resources (files published by the community) associated with the dataset", + description: "All the community resources (published by the community) associated with the dataset", items: CommunityResource - } + }, + covered_area: CoveredArea.schema() } + + if details do + base + |> Map.put(:history, %Schema{type: :array, items: ResourceHistory}) + else + base + end + end + end + + defmodule DatasetSummary do + @moduledoc false + require OpenApiSpex + + @properties DatasetUtils.get_dataset_prop(details: false) + + OpenApiSpex.schema(%{ + title: "DatasetSummary", + description: "A dataset is a composed of one or more resources (summarized version)", + type: :object, + required: @properties |> Map.keys(), + properties: @properties, + additionalProperties: false + }) + end + + defmodule DatasetDetails do + @moduledoc false + require OpenApiSpex + + @properties DatasetUtils.get_dataset_prop(details: true) + + OpenApiSpex.schema(%{ + title: "DatasetDetails", + description: + "A dataset is a composed of one or more resources (detailed version, including history & conversions).", + type: :object, + required: @properties |> Map.keys(), + properties: @properties, + additionalProperties: false + }) + end + + defmodule DatasetsResponse do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "DatasetsResponse", + type: :array, + items: DatasetSummary.schema() }) end @@ -368,15 +794,19 @@ defmodule TransportWeb.API.Schemas do @moduledoc false require OpenApiSpex + @properties %{ + url: %Schema{type: :string, description: "URL of the Resource"}, + type: %Schema{type: :string, description: "type of the resource (commune, region, aom)"}, + name: %Schema{type: :string, description: "name of the resource"} + } + OpenApiSpex.schema(%{ title: "Autocomplete result", description: "One result of the autocomplete", type: :object, - properties: %{ - url: %Schema{type: :string, description: "URL of the Resource"}, - type: %Schema{type: :string, description: "type of the resource (commune, region, aom)"}, - name: %Schema{type: :string, description: "name of the resource"} - } + required: @properties |> Map.keys(), + properties: @properties, + additionalProperties: false }) end @@ -385,7 +815,7 @@ defmodule TransportWeb.API.Schemas do require OpenApiSpex OpenApiSpex.schema(%{ - title: "Autocomplete results", + title: "AutocompleteResponse", description: "An array of matching results", type: :array, items: AutocompleteItem diff --git a/apps/transport/lib/transport_web/api/spec.ex b/apps/transport/lib/transport_web/api/spec.ex index b50a4f5af5..ca46605746 100644 --- a/apps/transport/lib/transport_web/api/spec.ex +++ b/apps/transport/lib/transport_web/api/spec.ex @@ -2,14 +2,26 @@ defmodule TransportWeb.API.Spec do @moduledoc """ OpenAPI specifications """ - alias OpenApiSpex.{Info, OpenApi, Paths} + alias OpenApiSpex.{Contact, Info, OpenApi, Paths} @spec spec :: OpenApiSpex.OpenApi.t() def spec do %OpenApi{ + # # https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#info-object info: %Info{ - title: "Transport.data.gouv.fr API", - version: "1.0" + title: "transport.data.gouv.fr API", + version: "1.0", + description: ~s""" + Extra documentation. + + The structure of the returned data is detailed at the bottom (see `Schemas`) and on each query (click on `Schema` near `Example Value`). + + To create a query, add the domain `https://transport.data.gouv.fr` and the path (e.g. `/api/datasets`). + """, + contact: %Contact{ + name: "API email support", + email: Application.fetch_env!(:transport, :contact_email) + } }, paths: Paths.from_router(TransportWeb.API.Router) } diff --git a/apps/transport/lib/transport_web/router.ex b/apps/transport/lib/transport_web/router.ex index 973c1d75bb..3d509f9c0c 100644 --- a/apps/transport/lib/transport_web/router.ex +++ b/apps/transport/lib/transport_web/router.ex @@ -45,7 +45,14 @@ defmodule TransportWeb.Router do scope "/", OpenApiSpex.Plug do pipe_through(:browser_no_csp) - get("/swaggerui", SwaggerUI, path: "/api/openapi") + + # NOTE: version of SwaggerUI is currently hardcoded by the Elixir package. + # See: https://github.com/open-api-spex/open_api_spex/issues/559 + get("/swaggerui", SwaggerUI, + path: "/api/openapi", + # See: https://github.com/etalab/transport-site/issues/3421 + syntax_highlight: false + ) end scope "/", TransportWeb do diff --git a/apps/transport/lib/transport_web/templates/dataset/index.html.heex b/apps/transport/lib/transport_web/templates/dataset/index.html.heex index bdc826bfc3..b483ecb67c 100644 --- a/apps/transport/lib/transport_web/templates/dataset/index.html.heex +++ b/apps/transport/lib/transport_web/templates/dataset/index.html.heex @@ -234,9 +234,12 @@