defmodule WandererAppWeb.OpenApiV1Spec do @moduledoc """ OpenAPI spec specifically for v1 JSON:API endpoints generated by AshJsonApi. """ @behaviour OpenApiSpex.OpenApi alias OpenApiSpex.{OpenApi, Info, Server, Components} @impl OpenApiSpex.OpenApi def spec do # This is called by the modify_open_api option in the router # We should return the spec from WandererAppWeb.OpenApi module WandererAppWeb.OpenApi.spec() end defp generate_spec_manually do %OpenApi{ info: %Info{ title: "WandererApp v1 JSON:API", version: "1.0.0", description: """ JSON:API compliant endpoints for WandererApp. ## Features - Filtering: Use `filter[attribute]=value` parameters - Sorting: Use `sort=attribute` or `sort=-attribute` for descending - Pagination: Use `page[limit]=n` and `page[offset]=n` - Relationships: Include related resources with `include=relationship` ## Authentication All endpoints require Bearer token authentication: ``` Authorization: Bearer YOUR_API_KEY ``` """ }, servers: [ Server.from_endpoint(WandererAppWeb.Endpoint) ], paths: get_v1_paths(), components: %Components{ schemas: get_v1_schemas(), securitySchemes: %{ "bearerAuth" => %{ "type" => "http", "scheme" => "bearer", "description" => "Map API key for authentication" } } }, security: [%{"bearerAuth" => []}], tags: get_v1_tags() } end defp get_v1_tags do [ %{"name" => "Access Lists", "description" => "Access control list management"}, %{"name" => "Access List Members", "description" => "ACL member management"}, %{"name" => "Characters", "description" => "Character management"}, %{"name" => "Maps", "description" => "Map management"}, %{"name" => "Map Systems", "description" => "Map system operations"}, %{"name" => "Map Connections", "description" => "System connection management"}, %{"name" => "Map Solar Systems", "description" => "Solar system data"}, %{"name" => "Map System Signatures", "description" => "Wormhole signature tracking"}, %{"name" => "Map System Structures", "description" => "Structure management"}, %{"name" => "Map System Comments", "description" => "System comments"}, %{"name" => "Map Character Settings", "description" => "Character map settings"}, %{"name" => "Map User Settings", "description" => "User map preferences"}, %{"name" => "Map Subscriptions", "description" => "Map subscription management"}, %{"name" => "Map Access Lists", "description" => "Map-specific ACLs"}, %{"name" => "Map States", "description" => "Map state information"}, %{"name" => "Users", "description" => "User management"}, %{"name" => "User Activities", "description" => "User activity tracking"}, %{"name" => "Ship Type Info", "description" => "Ship type information"} ] end defp get_v1_paths do # Generate paths for all resources resources = [ {"access_lists", "Access Lists"}, {"access_list_members", "Access List Members"}, {"characters", "Characters"}, {"maps", "Maps"}, {"map_systems", "Map Systems"}, {"map_connections", "Map Connections"}, {"map_solar_systems", "Map Solar Systems"}, {"map_system_signatures", "Map System Signatures"}, {"map_system_structures", "Map System Structures"}, {"map_system_comments", "Map System Comments"}, {"map_character_settings", "Map Character Settings"}, {"map_user_settings", "Map User Settings"}, {"map_subscriptions", "Map Subscriptions"}, {"map_access_lists", "Map Access Lists"}, {"map_states", "Map States"}, {"users", "Users"}, {"user_activities", "User Activities"}, {"ship_type_infos", "Ship Type Info"} ] Enum.reduce(resources, %{}, fn {resource, tag}, acc -> base_path = "/api/v1/#{resource}" paths = %{ base_path => %{ "get" => %{ "summary" => "List #{resource}", "tags" => [tag], "parameters" => get_standard_list_parameters(resource), "responses" => %{ "200" => %{ "description" => "List of #{resource}", "content" => %{ "application/vnd.api+json" => %{ "schema" => %{ "$ref" => "#/components/schemas/#{String.capitalize(resource)}ListResponse" } } } } } }, "post" => %{ "summary" => "Create #{String.replace(resource, "_", " ")}", "tags" => [tag], "requestBody" => %{ "required" => true, "content" => %{ "application/vnd.api+json" => %{ "schema" => %{ "$ref" => "#/components/schemas/#{String.capitalize(resource)}CreateRequest" } } } }, "responses" => %{ "201" => %{"description" => "Created"} } } }, "#{base_path}/{id}" => %{ "get" => %{ "summary" => "Get #{String.replace(resource, "_", " ")}", "tags" => [tag], "parameters" => [ %{ "name" => "id", "in" => "path", "required" => true, "schema" => %{"type" => "string"} } ], "responses" => %{ "200" => %{"description" => "Resource details"} } }, "patch" => %{ "summary" => "Update #{String.replace(resource, "_", " ")}", "tags" => [tag], "parameters" => [ %{ "name" => "id", "in" => "path", "required" => true, "schema" => %{"type" => "string"} } ], "requestBody" => %{ "required" => true, "content" => %{ "application/vnd.api+json" => %{ "schema" => %{ "$ref" => "#/components/schemas/#{String.capitalize(resource)}UpdateRequest" } } } }, "responses" => %{ "200" => %{"description" => "Updated"} } }, "delete" => %{ "summary" => "Delete #{String.replace(resource, "_", " ")}", "tags" => [tag], "parameters" => [ %{ "name" => "id", "in" => "path", "required" => true, "schema" => %{"type" => "string"} } ], "responses" => %{ "204" => %{"description" => "Deleted"} } } } } Map.merge(acc, paths) end) |> add_custom_paths() end defp add_custom_paths(paths) do # Add custom action paths custom_paths = %{ "/api/v1/maps/{id}/duplicate" => %{ "post" => %{ "summary" => "Duplicate map", "tags" => ["Maps"], "parameters" => [ %{ "name" => "id", "in" => "path", "required" => true, "schema" => %{"type" => "string"} } ], "responses" => %{ "201" => %{"description" => "Map duplicated"} } } }, "/api/v1/maps/{map_id}/systems_and_connections" => %{ "get" => %{ "summary" => "Get Map Systems and Connections", "description" => "Retrieve both systems and connections for a map in a single response", "tags" => ["Maps"], "parameters" => [ %{ "name" => "map_id", "in" => "path", "required" => true, "schema" => %{"type" => "string"}, "description" => "Map ID" } ], "responses" => %{ "200" => %{ "description" => "Combined systems and connections data", "content" => %{ "application/json" => %{ "schema" => %{ "type" => "object", "properties" => %{ "systems" => %{ "type" => "array", "items" => %{ "type" => "object", "properties" => %{ "id" => %{"type" => "string"}, "solar_system_id" => %{"type" => "integer"}, "name" => %{"type" => "string"}, "status" => %{"type" => "string"}, "visible" => %{"type" => "boolean"}, "locked" => %{"type" => "boolean"}, "position_x" => %{"type" => "integer"}, "position_y" => %{"type" => "integer"} } } }, "connections" => %{ "type" => "array", "items" => %{ "type" => "object", "properties" => %{ "id" => %{"type" => "string"}, "solar_system_source" => %{"type" => "integer"}, "solar_system_target" => %{"type" => "integer"}, "type" => %{"type" => "string"}, "time_status" => %{"type" => "string"}, "mass_status" => %{"type" => "string"} } } } } } } } }, "404" => %{"description" => "Map not found"}, "401" => %{"description" => "Unauthorized"} } } } } Map.merge(paths, custom_paths) end defp get_standard_list_parameters(resource) do base_params = [ %{ "name" => "sort", "in" => "query", "description" => "Sort results (e.g., 'name', '-created_at')", "schema" => %{"type" => "string"} }, %{ "name" => "page[limit]", "in" => "query", "description" => "Number of results per page", "schema" => %{"type" => "integer", "default" => 50} }, %{ "name" => "page[offset]", "in" => "query", "description" => "Offset for pagination", "schema" => %{"type" => "integer", "default" => 0} }, %{ "name" => "include", "in" => "query", "description" => "Include related resources (comma-separated)", "schema" => %{"type" => "string"} } ] # Add resource-specific filter parameters filter_params = case resource do "characters" -> [ %{ "name" => "filter[name]", "in" => "query", "description" => "Filter by character name", "schema" => %{"type" => "string"} }, %{ "name" => "filter[user_id]", "in" => "query", "description" => "Filter by user ID", "schema" => %{"type" => "string"} } ] "maps" -> [ %{ "name" => "filter[scope]", "in" => "query", "description" => "Filter by map scope", "schema" => %{"type" => "string"} }, %{ "name" => "filter[archived]", "in" => "query", "description" => "Filter by archived status", "schema" => %{"type" => "boolean"} } ] "map_systems" -> [ %{ "name" => "filter[map_id]", "in" => "query", "description" => "Filter by map ID", "schema" => %{"type" => "string"} }, %{ "name" => "filter[solar_system_id]", "in" => "query", "description" => "Filter by solar system ID", "schema" => %{"type" => "integer"} } ] "map_connections" -> [ %{ "name" => "filter[map_id]", "in" => "query", "description" => "Filter by map ID", "schema" => %{"type" => "string"} }, %{ "name" => "filter[source_id]", "in" => "query", "description" => "Filter by source system ID", "schema" => %{"type" => "string"} }, %{ "name" => "filter[target_id]", "in" => "query", "description" => "Filter by target system ID", "schema" => %{"type" => "string"} } ] "map_system_signatures" -> [ %{ "name" => "filter[system_id]", "in" => "query", "description" => "Filter by system ID", "schema" => %{"type" => "string"} }, %{ "name" => "filter[type]", "in" => "query", "description" => "Filter by signature type", "schema" => %{"type" => "string"} } ] _ -> [] end base_params ++ filter_params end defp get_v1_schemas do %{ # Generic JSON:API response wrapper "JsonApiWrapper" => %{ "type" => "object", "properties" => %{ "data" => %{ "type" => "object", "description" => "Primary data" }, "included" => %{ "type" => "array", "description" => "Included related resources" }, "meta" => %{ "type" => "object", "description" => "Metadata about the response" }, "links" => %{ "type" => "object", "description" => "Links for pagination and relationships" } } }, # Character schemas "CharacterResource" => %{ "type" => "object", "properties" => %{ "type" => %{"type" => "string", "enum" => ["characters"]}, "id" => %{"type" => "string"}, "attributes" => %{ "type" => "object", "properties" => %{ "name" => %{"type" => "string"}, "eve_id" => %{"type" => "integer"}, "corporation_id" => %{"type" => "integer"}, "alliance_id" => %{"type" => "integer"}, "online" => %{"type" => "boolean"}, "location" => %{"type" => "object"}, "inserted_at" => %{"type" => "string", "format" => "date-time"}, "updated_at" => %{"type" => "string", "format" => "date-time"} } }, "relationships" => %{ "type" => "object", "properties" => %{ "user" => %{ "type" => "object", "properties" => %{ "data" => %{ "type" => "object", "properties" => %{ "type" => %{"type" => "string"}, "id" => %{"type" => "string"} } } } } } } } }, "CharactersListResponse" => %{ "type" => "object", "properties" => %{ "data" => %{ "type" => "array", "items" => %{"$ref" => "#/components/schemas/CharacterResource"} }, "meta" => %{ "type" => "object", "properties" => %{ "page" => %{ "type" => "object", "properties" => %{ "offset" => %{"type" => "integer"}, "limit" => %{"type" => "integer"}, "total" => %{"type" => "integer"} } } } } } }, # Map schemas "MapResource" => %{ "type" => "object", "properties" => %{ "type" => %{"type" => "string", "enum" => ["maps"]}, "id" => %{"type" => "string"}, "attributes" => %{ "type" => "object", "properties" => %{ "name" => %{"type" => "string"}, "slug" => %{"type" => "string"}, "scope" => %{"type" => "string"}, "public_key" => %{"type" => "string"}, "archived" => %{"type" => "boolean"}, "inserted_at" => %{"type" => "string", "format" => "date-time"}, "updated_at" => %{"type" => "string", "format" => "date-time"} } }, "relationships" => %{ "type" => "object", "properties" => %{ "owner" => %{ "type" => "object" }, "characters" => %{ "type" => "object" }, "acls" => %{ "type" => "object" } } } } } } end end