{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openspatials.com/developers/spec/0.5/openspatials-listing.schema.json",
  "title": "OpenSpatials world listing, version 0.5",
  "description": "One world's entry in the registry at /worlds/<owner>/<slug>/listing.json (APP-PROFILE-AND-DIRECTORY.md section 5). The creator prepares owner, slug, name, kind, play and the optional fields; the registry adds id and links when it publishes the listing. The world's own profile (openspatials-world.json) decides what the world accepts; a listing only says where the world is. Limit 16 KiB, checked on the bytes before parsing. Unknown fields are allowed here and dropped when the registry publishes the listing. Addresses and the no-code rule reuse the directory list's definitions (openspatials-directory.schema.json).",
  "type": "object",
  "allOf": [
    { "$ref": "https://openspatials.com/developers/spec/0.5/openspatials-directory.schema.json#/$defs/noCode" }
  ],
  "required": ["openspatials", "owner", "slug", "name", "kind", "play"],
  "properties": {
    "openspatials": {
      "description": "The format version.",
      "x-openspatials-reason": "unknown-version",
      "enum": ["0.5"]
    },
    "id": {
      "description": "<owner>/<slug>. Added by the registry; when present it must equal owner and slug.",
      "$ref": "#/$defs/worldId"
    },
    "owner": {
      "description": "The creator's GitHub username in lowercase (hosting rules, section 2). Reserved owner names are checked by code.",
      "type": "string",
      "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$"
    },
    "slug": {
      "description": "The world's short name in its address: 1 to 40 lowercase letters, digits and hyphens.",
      "type": "string",
      "pattern": "^[a-z0-9-]{1,40}$"
    },
    "name": {
      "description": "The name people see.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "\\S"
    },
    "kind": {
      "description": "What the world is: game, metaverse, layer, fabric, twin or a new word. Data, never part of the address (taxonomy rule 3).",
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]{0,31}$"
    },
    "description": {
      "description": "One or two plain sentences.",
      "type": "string",
      "maxLength": 500
    },
    "play": {
      "description": "The crossing entry point: the page a visitor arrives at.",
      "$ref": "https://openspatials.com/developers/spec/0.5/openspatials-directory.schema.json#/$defs/gameUrl"
    },
    "profile": {
      "description": "The address of the world's profile, when it is not next to the play page.",
      "$ref": "https://openspatials.com/developers/spec/0.5/openspatials-directory.schema.json#/$defs/gameUrl"
    },
    "engine": {
      "type": "string",
      "maxLength": 32,
      "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._+-]*$"
    },
    "thumbnail": {
      "description": "Optional picture. An empty string means none.",
      "anyOf": [
        { "const": "" },
        { "$ref": "https://openspatials.com/developers/spec/0.5/openspatials-directory.schema.json#/$defs/gameUrl" }
      ]
    },
    "tags": {
      "type": "array",
      "maxItems": 10,
      "uniqueItems": true,
      "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" }
    },
    "on": {
      "description": "For a layer: the world it sits on, as <owner>/<slug>. That world must be in the registry.",
      "$ref": "#/$defs/worldId"
    },
    "location": {
      "description": "For a world of a real place: a GeoJSON geometry (RFC 7946), longitude then latitude in WGS 84.",
      "$ref": "#/$defs/geometry"
    },
    "places": {
      "description": "0.5 addition (registry support next): the places the world runs (WORLDS-WITHOUT-AN-ADDRESS.md section 2), as in its profile. The catalog repeats them, so a reader finds a world by any of its places (section 3.4).",
      "type": "array",
      "maxItems": 16,
      "items": { "$ref": "#/$defs/place" }
    },
    "signedProfile": {
      "description": "0.5 addition (registry support next): the world's signed profile carried by value, for a world that cannot serve its own files (WORLDS-WITHOUT-AN-ADDRESS.md section 3.2): the signed app profile of APP-PROFILE-AND-DIRECTORY.md section 1.5 as an object, whose subject is the world's identity (the profile's id). Its shape and signature are checked by readers (openspatials-signed-app-profile.schema.json and the reading order of section 3.3), not by this schema. The catalog does not repeat it. A listing that carries it may be at most 80 KiB.",
      "type": "object"
    },
    "added": { "type": "string", "format": "date-time" },
    "updated": { "type": "string", "format": "date-time" },
    "links": {
      "description": "Added by the registry: self, the page (alternate), the catalog (parent and root).",
      "type": "array",
      "maxItems": 16,
      "items": { "$ref": "#/$defs/link" }
    }
  },
  "$defs": {
    "place": {
      "description": "One place the world runs (WORLDS-WITHOUT-AN-ADDRESS.md section 2). kind web: a web page that runs the world, an https address (http on loopback). kind app: an app link an installed app opens; kind platform: a link into a platform or store that starts the world. For app and platform, any scheme but javascript, vbscript, data, file, blob, about, filesystem and http (unsafe-file). A reader ignores a place of a kind it does not know. The same definition is copied in openspatials-listing.schema.json; a test keeps the copies equal.",
      "type": "object",
      "required": [
        "url",
        "kind"
      ],
      "properties": {
        "url": {
          "description": "Where the place starts.",
          "type": "string",
          "maxLength": 2048,
          "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:\\S+$"
        },
        "kind": {
          "description": "web, app or platform.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]{0,31}$"
        },
        "name": {
          "description": "The label people see.",
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "pattern": "\\S"
        },
        "host": {
          "description": "The host's name for people, such as itch.io.",
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "pattern": "\\S"
        },
        "arrives": {
          "description": "True when the place takes a crossing link. Default false.",
          "type": "boolean"
        },
        "network": {
          "description": "What the world can reach there: full (default), own-files or none.",
          "enum": [
            "full",
            "own-files",
            "none"
          ]
        }
      },
      "if": {
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "const": "web"
          }
        }
      },
      "then": {
        "properties": {
          "url": {
            "$ref": "https://openspatials.com/developers/spec/0.5/openspatials-directory.schema.json#/$defs/gameUrl"
          }
        }
      },
      "else": {
        "properties": {
          "url": {
            "x-openspatials-reason": "unsafe-file",
            "not": {
              "pattern": "^(?:[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]|[Vv][Bb][Ss][Cc][Rr][Ii][Pp][Tt]|[Dd][Aa][Tt][Aa]|[Ff][Ii][Ll][Ee]|[Bb][Ll][Oo][Bb]|[Aa][Bb][Oo][Uu][Tt]|[Ff][Ii][Ll][Ee][Ss][Yy][Ss][Tt][Ee][Mm]|[Hh][Tt][Tt][Pp]):"
            }
          }
        }
      }
    },
    "worldId": {
      "type": "string",
      "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}/[a-z0-9-]{1,40}$"
    },
    "link": {
      "description": "A typed link, as in STAC and RFC 8288: relation, address, media type, title.",
      "type": "object",
      "required": ["rel", "href"],
      "properties": {
        "rel": { "type": "string", "pattern": "^[a-z][a-z0-9.-]{0,31}$" },
        "href": { "type": "string", "format": "uri", "maxLength": 2048 },
        "type": { "type": "string", "maxLength": 100 },
        "title": { "type": "string", "maxLength": 200 }
      }
    },
    "position": {
      "type": "array",
      "minItems": 2,
      "maxItems": 3,
      "prefixItems": [
        { "type": "number", "minimum": -180, "maximum": 180 },
        { "type": "number", "minimum": -90, "maximum": 90 },
        { "type": "number" }
      ]
    },
    "positions": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/position" } },
    "line": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/position" } },
    "ring": { "type": "array", "minItems": 4, "items": { "$ref": "#/$defs/position" } },
    "polygon": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ring" } },
    "geometry": {
      "type": "object",
      "required": ["type", "coordinates"],
      "properties": {
        "type": { "enum": ["Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon"] },
        "coordinates": { "type": "array" }
      },
      "allOf": [
        { "if": { "required": ["type"], "properties": { "type": { "const": "Point" } } }, "then": { "properties": { "coordinates": { "$ref": "#/$defs/position" } } } },
        { "if": { "required": ["type"], "properties": { "type": { "const": "MultiPoint" } } }, "then": { "properties": { "coordinates": { "$ref": "#/$defs/positions" } } } },
        { "if": { "required": ["type"], "properties": { "type": { "const": "LineString" } } }, "then": { "properties": { "coordinates": { "$ref": "#/$defs/line" } } } },
        { "if": { "required": ["type"], "properties": { "type": { "const": "MultiLineString" } } }, "then": { "properties": { "coordinates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/line" } } } } },
        { "if": { "required": ["type"], "properties": { "type": { "const": "Polygon" } } }, "then": { "properties": { "coordinates": { "$ref": "#/$defs/polygon" } } } },
        { "if": { "required": ["type"], "properties": { "type": { "const": "MultiPolygon" } } }, "then": { "properties": { "coordinates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/polygon" } } } } }
      ]
    }
  }
}
