diff --git a/data/scenarios/doc-fragments/SCHEMA.md b/data/scenarios/doc-fragments/SCHEMA.md index 2420613d20..9d779f625b 100644 --- a/data/scenarios/doc-fragments/SCHEMA.md +++ b/data/scenarios/doc-fragments/SCHEMA.md @@ -43,12 +43,12 @@ Description of an entity in the Swarm game | `combustion` | | [combustion](#combustion "Link to object properties") | Properties of combustion. | | `description` | | \[`string`\] | A description of the entity, as a list of paragraphs. | | `display` | | [display](#display "Link to object properties") | Display information for the entity. | -| `growth` | `"null"` | `array` | For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown. | +| `growth` | | `array` | For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown. | | `name` | | `string` | The name of the entity. This is what will show up in the inventory and how the entity can be referred to. | -| `orientation` | `"null"` | `array` | A 2-tuple of integers specifying an orientation vector for the entity. Currently unused. | -| `plural` | `"null"` | `string` | An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name. | +| `orientation` | | `array` | A 2-tuple of integers specifying an orientation vector for the entity. Currently unused. | +| `plural` | | `string` | An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name. | | `properties` | `[]` | \[`string`\] | A list of properties of this entity. | -| `yields` | `"null"` | `string` | The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself. | +| `yields` | | `string` | The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself. | #### Entity properties @@ -83,29 +83,29 @@ the Swarm wiki. Properties of entity combustion -| Key | Default? | Type | Description | -|------------|-------------------------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| `duration` | `[Number 100.0,Number 200.0]` | [range](#numeric-range "Link to object properties") | For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time that the combustion shall persist. | -| `ignition` | `0.5` | `number` | Rate of ignition by a neighbor, per tick. | -| `product` | `"ash"` | `string` or `null` | What entity, if any, is left over after combustion | +| Key | Default? | Type | Description | +|------------|--------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `duration` | `[100, 200]` | [range](#numeric-range "Link to object properties") | For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time that the combustion shall persist. | +| `ignition` | `0.5` | `number` | Rate of ignition by a neighbor, per tick. | +| `product` | `"ash"` | `string` or `null` | What entity, if any, is left over after combustion | ### Robot Description of a robot in the Swarm game -| Key | Default? | Type | Description | -|---------------|---------------------------|--------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `description` | | `string` | A description of the robot. This is currently not used for much, other than scenario documentation. | -| `devices` | `[]` | \[`string`\] | A list of entity names which should be equipped as the robot's devices, i.e. entities providing capabilities to run commands and interpret language constructs. | -| `dir` | `[Number 0.0,Number 0.0]` | `array` | An optional starting orientation of the robot, expressed as a vector. Every time the robot executes a `move` command, this vector will be added to its position. Typically, this is a unit vector in one of the four cardinal directions, although there is no particular reason that it has to be. When omitted, the robot's direction will be the zero vector. | -| `display` | `"default"` | [display](#display "Link to object properties") | Display information for the robot. If this field is omitted, the default robot display will be used. | -| `heavy` | `False` | `boolean` | Whether the robot is heavy. Heavy robots require `tank treads` to `move` (rather than just `treads` for other robots). | -| `inventory` | `[]` | [inventory](#inventory "Link to object properties") | A list of `[count, entity name]` pairs, specifying the entities in the robot's starting inventory, and the number of each. | -| `loc` | | [cosmic-loc](#cosmic-location "Link to object properties") or [planar-loc](#planar-location "Link to object properties") | An optional starting location for the robot. If the `loc` field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map. | -| `name` | | `string` | The name of the robot. This shows up in the list of robots in the game (`F2`), and is also how the robot will be referred to in the world palette. | -| `program` | | `string` | This is the text of a Swarm program which the robot should initially run, and must be syntax- and type-error-free. If omitted, the robot will simply be idle. | -| `system` | `False` | `boolean` | Whether the robot is a "system" robot. System robots can do anything, without regard for devices and capabilities. System robots are invisible by default. | -| `unwalkable` | `[]` | \[`string`\] | A list of entities that this robot cannot walk across. | +| Key | Default? | Type | Description | +|---------------|-------------|--------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `description` | | `string` | A description of the robot. This is currently not used for much, other than scenario documentation. | +| `devices` | `[]` | \[`string`\] | A list of entity names which should be equipped as the robot's devices, i.e. entities providing capabilities to run commands and interpret language constructs. | +| `dir` | `[0, 0]` | `array` | An optional starting orientation of the robot, expressed as a vector. Every time the robot executes a `move` command, this vector will be added to its position. Typically, this is a unit vector in one of the four cardinal directions, although there is no particular reason that it has to be. When omitted, the robot's direction will be the zero vector. | +| `display` | `"default"` | [display](#display "Link to object properties") | Display information for the robot. If this field is omitted, the default robot display will be used. | +| `heavy` | `False` | `boolean` | Whether the robot is heavy. Heavy robots require `tank treads` to `move` (rather than just `treads` for other robots). | +| `inventory` | `[]` | [inventory](#inventory "Link to object properties") | A list of `[count, entity name]` pairs, specifying the entities in the robot's starting inventory, and the number of each. | +| `loc` | | [cosmic-loc](#cosmic-location "Link to object properties") or [planar-loc](#planar-location "Link to object properties") | An optional starting location for the robot. If the `loc` field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map. | +| `name` | | `string` | The name of the robot. This shows up in the list of robots in the game (`F2`), and is also how the robot will be referred to in the world palette. | +| `program` | | `string` | This is the text of a Swarm program which the robot should initially run, and must be syntax- and type-error-free. If omitted, the robot will simply be idle. | +| `system` | `False` | `boolean` | Whether the robot is a "system" robot. System robots can do anything, without regard for devices and capabilities. System robots are invisible by default. | +| `unwalkable` | `[]` | \[`string`\] | A list of entities that this robot cannot walk across. | #### Base robot @@ -147,26 +147,55 @@ table. | `curOrientation` | | `array` | Currently unused | | `invisible` | `False` | `boolean` | Whether the entity or robot should be invisible. Invisible entities and robots are not drawn, but can still be interacted with in otherwise normal ways. System robots are by default invisible. | | `orientationMap` | `fromList []` | [orientation-map](#orientation-map "Link to object properties") | | -| `priority` | `1.0` | `number` | When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority `1`, and robots have priority `10`. | +| `priority` | `1` | `number` | When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority `1`, and robots have priority `10`. | + +### Recipe + +Recipe describes a process that takes some inputs and produces some +outputs, which robots can access using `make` and `drill`. + +| Key | Default? | Type | Description | +|------------|----------|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `in` | | [inventory](#inventory "Link to object properties") | A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed. | +| `out` | | [inventory](#inventory "Link to object properties") | A list of outputs produced by the recipe. It is a list of `[count, entity name]` tuples just like `in`. | +| `required` | `[]` | [inventory](#inventory "Link to object properties") | A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of \[count, entity name\] tuples just like in and out. | +| `time` | `1` | `number` | The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will wait for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes. | +| `weight` | `1` | `number` | Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a widget, one with weight `1` and the other with weight `9`. When a robot executes `make "widget"`, the first recipe will be chosen 10% of the time, and the second recipe 90% of the time. | + +### Inventory + +A list of `[count, entity name]` pairs, specifying the number of each +entity. + +List of [entity-count](#entity-count "Link to object properties") + +### Entity count + +One row in an inventory list + +| Index | Type | Description | +|-------|----------|-------------| +| `0` | `number` | Quantity | +| `1` | `string` | Entity name | ### World Description of the world in the Swarm game -| Key | Default? | Type | Description | -|--------------|---------------------------|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `default` | | \[`string`\] | Default world cell content | -| `dsl` | | `string` | A term in the Swarm world description DSL. The world it describes will be layered underneath the world described by the rest of the fields. | -| `map` | `""` | `string` | A rectangular string, using characters from the palette, exactly specifying the contents of a rectangular portion of the world. Leading spaces are ignored. The rest of the world is either filled by the default cell, or by procedural generation otherwise. Note that this is optional; if omitted, the world will simply be filled with the default cell or procedurally generated. | -| `name` | | `string` | Name of this subworld | -| `offset` | `False` | `boolean` | Whether the base robot's position should be moved to the nearest "good" location, currently defined as a location near a `tree`, in a 16x16 patch which contains at least one each of `tree`, `copper ore`, `bit (0)`, `bit (1)`, `rock`, `lambda`, `water`, and `sand`. The classic scenario uses `offset: True` to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game (see [code](https://github.com/swarm-game/swarm/blob/e06e04f622a3762a10e7c942c1cbd2c1e396144f/src/Swarm/Game/World/Gen.hs#L79)). | -| `palette` | `fromList []` | `object` | The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See Cells for the contents of the tuples representing a cell. | -| `placements` | | \[[placement](#placement "Link to object properties")\] | Structure placements | -| `portals` | | \[[portal](#portal "Link to object properties")\] | A list of portal definitions that reference waypoints. | -| `scrollable` | `True` | `boolean` | Whether players are allowed to scroll the world map. | -| `structures` | | \[[named-structure](#named-structure "Link to object properties")\] | Structure definitions | -| `upperleft` | `[Number 0.0,Number 0.0]` | `array` | A 2-tuple of `int` values specifying the `(x,y)` coordinates of the upper left corner of the map. | -| `waypoints` | | \[[explicit-waypoint](#waypoint "Link to object properties")\] | Single-location waypoint definitions | +| Key | Default? | Type | Description | +|--------------|----------|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | | \[`string`\] | Default world cell content | +| `dsl` | | `string` | A term in the Swarm world description DSL. The world it describes will be layered underneath the world described by the rest of the fields. | +| `map` | `""` | `string` | A rectangular string, using characters from the palette, exactly specifying the contents of a rectangular portion of the world. Leading spaces are ignored. The rest of the world is either filled by the default cell, or by procedural generation otherwise. Note that this is optional; if omitted, the world will simply be filled with the default cell or procedurally generated. | +| `name` | | `string` | Name of this subworld | +| `offset` | `False` | `boolean` | Whether the base robot's position should be moved to the nearest "good" location, currently defined as a location near a `tree`, in a 16x16 patch which contains at least one each of `tree`, `copper ore`, `bit (0)`, `bit (1)`, `rock`, `lambda`, `water`, and `sand`. The classic scenario uses `offset: True` to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game (see [code](https://github.com/swarm-game/swarm/blob/e06e04f622a3762a10e7c942c1cbd2c1e396144f/src/Swarm/Game/World/Gen.hs#L79)). | +| `palette` | | `object` | The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See [Cells](#cells) for the contents of the tuples representing a cell. | +| `placements` | | \[[placement](#placement "Link to object properties")\] | Structure placements | +| `portals` | | \[[portal](#portal "Link to object properties")\] | A list of portal definitions that reference waypoints. | +| `scrollable` | `True` | `boolean` | Whether players are allowed to scroll the world map. | +| `structures` | | \[[named-structure](#named-structure "Link to object properties")\] | Structure definitions | +| `upperleft` | `[0, 0]` | `array` | A 2-tuple of `int` values specifying the `(x,y)` coordinates of the upper left corner of the map. | +| `waypoints` | | \[[explicit-waypoint](#waypoint "Link to object properties")\] | Single-location waypoint definitions | #### Cells @@ -225,27 +254,6 @@ Explicit waypoint definition | `loc` | | [planar-loc](#planar-location "Link to object properties") | | | `name` | | `string` | Waypoint name | -### Recipe - -Recipe describes a process that takes some inputs and produces some -outputs, which robots can access using `make` and `drill`. - -| Key | Default? | Type | Description | -|------------|----------|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `in` | | [inventory](#inventory "Link to object properties") | A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed. | -| `out` | | [inventory](#inventory "Link to object properties") | A list of outputs produced by the recipe. It is a list of `[count, entity name]` tuples just like `in`. | -| `required` | `[]` | [inventory](#inventory "Link to object properties") | A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of \[count, entity name\] tuples just like in and out. | -| `time` | `1.0` | `number` | The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will wait for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes. | -| `weight` | `1.0` | `number` | Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a widget, one with weight `1` and the other with weight `9`. When a robot executes `make "widget"`, the first recipe will be chosen 10% of the time, and the second recipe 90% of the time. | - -### Inventory - -A list of `[count, entity name]` pairs, specifying the number of each -entity. - -| Key | Default? | Type | Description | -|-----|----------|------|-------------| - ### Objective Scenario goals and their prerequisites. The top-level objectives field @@ -287,8 +295,10 @@ Structure placement x and y coordinates of a location in a particular world -| Key | Default? | Type | Description | -|-----|----------|------|-------------| +| Index | Type | Description | +|-------|----------|--------------| +| `0` | `number` | X coordinate | +| `1` | `number` | Y coordinate | ### Portal @@ -314,15 +324,14 @@ Properties of a portal's exit Prerequisite conditions for an objective. -| Key | Default? | Type | Description | -|-----|----------|------|-------------| - ### Numeric range Min/max range of a value -| Key | Default? | Type | Description | -|-----|----------|------|-------------| +| Index | Type | Description | +|-------|----------|-------------| +| `0` | `number` | minimum | +| `1` | `number` | maximum | ### Structure orientation diff --git a/data/schema/entity-count.json b/data/schema/entity-count.json new file mode 100644 index 0000000000..f04a137429 --- /dev/null +++ b/data/schema/entity-count.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entity-count.json", + "title": "Entity count", + "description": "One row in an inventory list", + "type": "array", + "items": [ + { + "name": "Quantity", + "type": "number" + }, + { + "name": "Entity name", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/data/schema/entity.json b/data/schema/entity.json index a532a76af0..2af23964c1 100644 --- a/data/schema/entity.json +++ b/data/schema/entity.json @@ -20,7 +20,7 @@ "description": "Display information for the entity." }, "plural": { - "default": "null", + "default": null, "type": "string", "description": "An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name." }, @@ -32,7 +32,7 @@ "description": "A description of the entity, as a list of paragraphs." }, "orientation": { - "default": "null", + "default": null, "type": "array", "items": [ { @@ -47,7 +47,7 @@ "description": "A 2-tuple of integers specifying an orientation vector for the entity. Currently unused." }, "growth": { - "default": "null", + "default": null, "type": "array", "items": [ { @@ -67,7 +67,7 @@ "description": "Properties of combustion." }, "yields": { - "default": "null", + "default": null, "type": "string", "description": "The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself." }, diff --git a/data/schema/inventory.json b/data/schema/inventory.json index 70b79caa15..5b67e00a46 100644 --- a/data/schema/inventory.json +++ b/data/schema/inventory.json @@ -5,16 +5,6 @@ "description": "A list of `[count, entity name]` pairs, specifying the number of each entity.", "type": "array", "items": { - "type": "array", - "items": [ - { - "title": "Entity count", - "type": "number" - }, - { - "title": "Entity name", - "type": "string" - } - ] + "$ref": "entity-count.json" } } \ No newline at end of file diff --git a/data/schema/world.json b/data/schema/world.json index 791eaae830..3144d5ce9d 100644 --- a/data/schema/world.json +++ b/data/schema/world.json @@ -57,10 +57,9 @@ "description": "Whether players are allowed to scroll the world map." }, "palette": { - "default": {}, "type": "object", "examples": [{"T": ["grass", "tree"]}], - "description": "The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See Cells for the contents of the tuples representing a cell." + "description": "The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See [Cells](#cells) for the contents of the tuples representing a cell." }, "portals": { "description": "A list of portal definitions that reference waypoints.", diff --git a/src/Swarm/Doc/Gen.hs b/src/Swarm/Doc/Gen.hs index 3ca2d92081..d232db89dc 100644 --- a/src/Swarm/Doc/Gen.hs +++ b/src/Swarm/Doc/Gen.hs @@ -43,6 +43,7 @@ import Data.Text.IO qualified as T import Data.Tuple (swap) import Swarm.Doc.Pedagogy import Swarm.Doc.Schema.Render +import Swarm.Doc.Util import Swarm.Game.Display (displayChar) import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E @@ -225,12 +226,6 @@ generateSpecialKeyNames = -- GENERATE TABLES: COMMANDS, ENTITIES AND CAPABILITIES TO MARKDOWN TABLE -- ---------------------------------------------------------------------------- -wrap :: Char -> Text -> Text -wrap c = T.cons c . flip T.snoc c - -codeQuote :: Text -> Text -codeQuote = wrap '`' - escapeTable :: Text -> Text escapeTable = T.concatMap (\c -> if c == '|' then T.snoc "\\" c else T.singleton c) @@ -245,12 +240,6 @@ listToRow mw xs = wrap '|' . T.intercalate "|" $ zipWith format mw xs maxWidths :: [[Text]] -> [Int] maxWidths = map (maximum . map T.length) . transpose -addLink :: Text -> Text -> Text -addLink l t = T.concat ["[", t, "](", l, ")"] - -tshow :: (Show a) => a -> Text -tshow = T.pack . show - -- --------- -- COMMANDS -- --------- diff --git a/src/Swarm/Doc/Schema/Arrangement.hs b/src/Swarm/Doc/Schema/Arrangement.hs index 6e0c18b20a..8e1697efa5 100644 --- a/src/Swarm/Doc/Schema/Arrangement.hs +++ b/src/Swarm/Doc/Schema/Arrangement.hs @@ -3,12 +3,13 @@ -- -- Graph-based heuristics for arranging the -- order of sections in the schema docs -module Swarm.Doc.Schema.Arrangement (mkSchemaGraph) where +module Swarm.Doc.Schema.Arrangement (sortAndPruneSchemas) where import Data.Graph import Data.Set qualified as Set import Swarm.Doc.Schema.Parse import Swarm.Doc.Schema.Refined +import Swarm.Doc.Schema.SchemaType -- | Sort the schemas in topological order. -- @@ -17,18 +18,16 @@ import Swarm.Doc.Schema.Refined -- (i.e. exclude @entities.json@ and @recipes.json@, -- which are used independently to validate @entities.yaml@ -- and @recipes.yaml@). -mkSchemaGraph :: +sortAndPruneSchemas :: SchemaIdReference -> [SchemaData] -> [SchemaData] -mkSchemaGraph rootSchemaKey schemas = +sortAndPruneSchemas rootSchemaKey schemas = reverse . flattenSCCs . stronglyConnComp $ reachableEdges where rawEdgeList = map getNodeEdgesEntry schemas (graph, _nodeFromVertex, vertexFromKey) = graphFromEdges rawEdgeList - - scenarioVertex = vertexFromKey rootSchemaKey - reachableVertices = Set.fromList $ maybe [] (reachable graph) scenarioVertex + reachableVertices = Set.fromList $ maybe [] (reachable graph) $ vertexFromKey rootSchemaKey reachableEdges = filter f rawEdgeList f (_, k, _) = maybe False (`Set.member` reachableVertices) . vertexFromKey $ k diff --git a/src/Swarm/Doc/Schema/Parse.hs b/src/Swarm/Doc/Schema/Parse.hs index cbdba04d91..75bf3dcab6 100644 --- a/src/Swarm/Doc/Schema/Parse.hs +++ b/src/Swarm/Doc/Schema/Parse.hs @@ -11,17 +11,12 @@ module Swarm.Doc.Schema.Parse where import Control.Applicative ((<|>)) import Data.Aeson -import Data.List.Extra (replace) import Data.Map (Map) -import Data.Map qualified as M -import Data.Maybe (fromMaybe, mapMaybe) -import Data.Set (Set) -import Data.Set qualified as Set +import Data.Maybe (fromMaybe) import Data.Text (Text) -import Data.Text qualified as T -import GHC.Generics (Generic) import Swarm.Doc.Schema.Refined -import System.FilePath (takeBaseName) +import Swarm.Doc.Schema.SchemaType +import Swarm.Doc.Schema.Superset import Text.Pandoc -- | Includes everything needed to @@ -32,48 +27,17 @@ data SchemaData = SchemaData , markdownFooters :: [Pandoc] } -schemaJsonOptions :: Options -schemaJsonOptions = - defaultOptions - { fieldLabelModifier = replace "S" "$" . tail -- drops leading underscore - } - -data ItemDescription - = ItemList [SwarmSchema] - | ItemType SwarmSchema +data Members + = ObjectProperties (Map Text SwarmSchema) + | ListMembers (ItemDescription SwarmSchema) + | SimpleType (SingleOrList Text) -- TODO: Currently unused deriving (Eq, Ord, Show) -instance FromJSON ItemDescription where - parseJSON x = - ItemType <$> parseJSON x - <|> ItemList <$> parseJSON x - -data SchemaRaw = SchemaRaw - { _description :: Maybe Text - , _default :: Maybe Value - , _title :: Maybe Text - , _type :: Maybe (SingleOrList Text) - , _properties :: Maybe (Map Text SwarmSchema) - , _items :: Maybe ItemDescription - , _examples :: Maybe [Value] - , _Sref :: Maybe Text - , _oneOf :: Maybe [SchemaRaw] - , _footers :: Maybe [FilePath] - } - deriving (Eq, Ord, Show, Generic) - -instance FromJSON SchemaRaw where - parseJSON = genericParseJSON schemaJsonOptions - --- | A subset of all JSON schemas, conforming to internal Swarm conventions. --- --- TODO: Conveniently, this extra layer of processing --- is able to enforce that all "object" definitions in the schema --- contain the @additionalProperties: false@ property. data ToplevelSchema = ToplevelSchema { title :: Text , description :: Pandoc , content :: SwarmSchema + , members :: Maybe Members , footerPaths :: [FilePath] } deriving (Eq, Ord, Show) @@ -86,71 +50,8 @@ instance FromJSON ToplevelSchema where theTitle <- maybe (fail "Schema requires a title") return $ _title rawSchema theDescription <- maybe (fail "Schema requires a description") return $ objectDescription swarmSchema let theFooters = fromMaybe [] $ _footers rawSchema - return $ ToplevelSchema theTitle theDescription swarmSchema theFooters - --- TODO use this to represent mutual-exclusivity --- between objects and arrays -data Members - = ObjectProperties (Map Text SwarmSchema) - | ListMembers [SwarmSchema] - -data SwarmSchema = SwarmSchema - { schemaType :: SchemaType - , defaultValue :: Maybe Value - , objectDescription :: Maybe Pandoc - , properties :: Maybe (Map Text SwarmSchema) - , examples :: [Value] - } - deriving (Eq, Ord, Show) - -instance FromJSON SwarmSchema where - parseJSON x = do - rawSchema :: rawSchema <- parseJSON x - toSwarmSchema rawSchema - -getMarkdown :: MonadFail m => Text -> m Pandoc -getMarkdown desc = case runPure (readMarkdown def desc) of - Right doc -> return doc - Left err -> fail $ T.unpack $ renderError err - -extractSchemaType :: SchemaRaw -> Maybe SchemaType -extractSchemaType rawSchema = - mkReference <$> _Sref rawSchema - <|> getTypeFromItems - <|> Simple <$> _type rawSchema - <|> Alternatives . mapMaybe extractSchemaType <$> _oneOf rawSchema - where - mkReference = Reference . SchemaIdReference . T.pack . takeBaseName . T.unpack - - getTypeFromItems :: Maybe SchemaType - getTypeFromItems = do - itemsThing <- _items rawSchema - case itemsThing of - ItemList _ -> Nothing - ItemType x -> Just $ ListOf $ schemaType x - -toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema -toSwarmSchema rawSchema = do - theType <- maybe (fail "Unspecified sub-schema type") return maybeType - markdownDescription <- mapM getMarkdown $ _description rawSchema - return - SwarmSchema - { schemaType = theType - , defaultValue = _default rawSchema - , objectDescription = markdownDescription - , examples = fromMaybe [] $ _examples rawSchema - , properties = _properties rawSchema - } - where - maybeType = extractSchemaType rawSchema - --- * Utilities - --- | Recursively extract references to other schemas -extractReferences :: SwarmSchema -> Set SchemaIdReference -extractReferences s = thisRefList <> otherRefLists - where - thisRefList = Set.fromList . getSchemaReferences $ schemaType s - otherSchemas = maybe [] M.elems (properties s) - otherRefLists = Set.unions $ map extractReferences otherSchemas + let maybeMembers = + ObjectProperties <$> properties swarmSchema + <|> ListMembers <$> itemsDescription swarmSchema + return $ ToplevelSchema theTitle theDescription swarmSchema maybeMembers theFooters diff --git a/src/Swarm/Doc/Schema/Refined.hs b/src/Swarm/Doc/Schema/Refined.hs index 6b6f98261a..8d4b4237d2 100644 --- a/src/Swarm/Doc/Schema/Refined.hs +++ b/src/Swarm/Doc/Schema/Refined.hs @@ -9,40 +9,67 @@ module Swarm.Doc.Schema.Refined where import Control.Applicative ((<|>)) import Data.Aeson -import Data.List (intersperse) +import Data.List.Extra (replace) import Data.Map (Map) import Data.Map qualified as M +import Data.Maybe (fromMaybe, mapMaybe) +import Data.Set (Set) +import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T +import GHC.Generics (Generic) +import Swarm.Doc.Schema.SchemaType +import Swarm.Doc.Schema.Superset import System.FilePath (takeBaseName) +import Text.Pandoc import Text.Pandoc.Builder -newtype SingleOrList a = SingleOrList - { getList :: [a] +-- * Basic + +schemaJsonOptions :: Options +schemaJsonOptions = + defaultOptions + { fieldLabelModifier = replace "S" "$" . drop 1 -- drops leading underscore + } + +-- | A single record that encompasses all possible objects +-- in a JSON schema. All fields are optional. +data SchemaRaw = SchemaRaw + { _description :: Maybe Text + , _default :: Maybe Value + , _title :: Maybe Text + , _type :: Maybe (SingleOrList Text) + , _name :: Maybe Text + , _properties :: Maybe (Map Text SwarmSchema) -- TODO Maybe this should be SchemaRaw + , _items :: Maybe (ItemDescription SwarmSchema) -- TODO Maybe this should be SchemaRaw + , _examples :: Maybe [Value] + , _Sref :: Maybe Text + , _oneOf :: Maybe [SchemaRaw] + , _footers :: Maybe [FilePath] + , _additionalProperties :: Maybe Bool } - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic) -instance (FromJSON a) => FromJSON (SingleOrList a) where - parseJSON x = - SingleOrList <$> do - (pure <$> parseJSON x) <|> parseJSON x +instance FromJSON SchemaRaw where + parseJSON = genericParseJSON schemaJsonOptions -newtype SchemaIdReference = SchemaIdReference Text - deriving (Eq, Ord, Show) +extractSchemaType :: SchemaRaw -> Maybe SchemaType +extractSchemaType rawSchema = + mkReference <$> _Sref rawSchema + <|> getTypeFromItems + <|> Simple <$> _type rawSchema + <|> Alternatives . mapMaybe extractSchemaType <$> _oneOf rawSchema + where + mkReference = Reference . SchemaIdReference . T.pack . takeBaseName . T.unpack -fromFilePath :: FilePath -> SchemaIdReference -fromFilePath = SchemaIdReference . T.pack . takeBaseName - -data SchemaType - = -- | A basic built-in type - Simple (SingleOrList Text) - | -- | Any one of multiple possible schema types - Alternatives [SchemaType] - | -- | A reference to a schema defined elsewhere - Reference SchemaIdReference - | -- | Members of a list, all of the given schema type - ListOf SchemaType - deriving (Eq, Ord, Show) + getTypeFromItems :: Maybe SchemaType + getTypeFromItems = do + itemsThing <- _items rawSchema + case itemsThing of + ItemList _ -> Nothing + ItemType x -> Just $ ListOf $ schemaType x + +-- * Refined getSchemaReferences :: SchemaType -> [SchemaIdReference] getSchemaReferences = \case @@ -51,18 +78,59 @@ getSchemaReferences = \case Reference x -> pure x ListOf x -> getSchemaReferences x -fragmentHref :: Map SchemaIdReference Text -> SchemaIdReference -> Text -fragmentHref titleMap r@(SchemaIdReference ref) = - T.cons '#' . T.toLower . T.replace " " "-" $ x +-- | A subset of all JSON schemas, conforming to internal Swarm conventions. +-- +-- Conveniently, this extra representation layer +-- is able to enforce (via 'toSwarmSchema') that all "object" +-- definitions in the schema contain the @"additionalProperties": true@ attribute. +data SwarmSchema = SwarmSchema + { schemaType :: SchemaType + , defaultValue :: Maybe Value + , objectDescription :: Maybe Pandoc + , properties :: Maybe (Map Text SwarmSchema) + , itemsDescription :: Maybe (ItemDescription SwarmSchema) + , examples :: [Value] + } + deriving (Eq, Ord, Show) + +instance FromJSON SwarmSchema where + parseJSON x = do + rawSchema :: rawSchema <- parseJSON x + toSwarmSchema rawSchema + +getMarkdown :: MonadFail m => Text -> m Pandoc +getMarkdown desc = case runPure (readMarkdown def desc) of + Right d -> return d + Left err -> fail $ T.unpack $ renderError err + +toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema +toSwarmSchema rawSchema = do + theType <- maybe (fail "Unspecified sub-schema type") return maybeType + markdownDescription <- mapM getMarkdown $ _description rawSchema + + if null (_properties rawSchema) || not (fromMaybe True (_additionalProperties rawSchema)) + then return () + else fail "All objects must specify '\"additionalProperties\": true'" + + return + SwarmSchema + { schemaType = theType + , defaultValue = _default rawSchema + , objectDescription = markdownDescription <|> doc . plain . text <$> _name rawSchema + , examples = fromMaybe [] $ _examples rawSchema + , properties = _properties rawSchema + , itemsDescription = _items rawSchema + } where - x = M.findWithDefault ref r titleMap - -listToText :: Map SchemaIdReference Text -> SchemaType -> Inlines -listToText titleMap = \case - Simple xs -> renderAlternatives $ map code $ getList xs - Alternatives xs -> renderAlternatives $ map (listToText titleMap) xs - Reference r@(SchemaIdReference x) -> schemaLink r x - ListOf x -> text "[" <> listToText titleMap x <> text "]" + maybeType = extractSchemaType rawSchema + +-- * Utilities + +-- | Recursively extract references to other schemas +extractReferences :: SwarmSchema -> Set SchemaIdReference +extractReferences s = thisRefList <> otherRefLists where - renderAlternatives = mconcat . intersperse (text " or ") - schemaLink r = link (fragmentHref titleMap r) "Link to object properties" . text + thisRefList = Set.fromList . getSchemaReferences $ schemaType s + + otherSchemas = maybe [] M.elems $ properties s + otherRefLists = Set.unions $ map extractReferences otherSchemas diff --git a/src/Swarm/Doc/Schema/Render.hs b/src/Swarm/Doc/Schema/Render.hs index 0b7cfb6a0d..ddea909a8c 100644 --- a/src/Swarm/Doc/Schema/Render.hs +++ b/src/Swarm/Doc/Schema/Render.hs @@ -11,15 +11,20 @@ import Control.Arrow (left, (&&&)) import Control.Monad.Except (liftIO, runExceptT) import Control.Monad.Trans.Except (except) import Data.Aeson +import Data.List (intersperse) import Data.Map (Map) import Data.Map.Strict qualified as M -import Data.Maybe (fromMaybe) +import Data.Scientific (FPFormat (..), Scientific, formatScientific) import Data.Text qualified as T import Data.Text.IO qualified as TIO +import Data.Vector qualified as V import Swarm.Doc.Schema.Arrangement import Swarm.Doc.Schema.Parse import Swarm.Doc.Schema.Refined -import Swarm.Util (quote, showT) +import Swarm.Doc.Schema.SchemaType +import Swarm.Doc.Schema.Superset +import Swarm.Doc.Util +import Swarm.Util (applyWhen, brackets, quote, showT) import System.Directory (listDirectory) import System.FilePath (splitExtension, (<.>), ()) import Text.Pandoc @@ -38,35 +43,65 @@ schemasDir = "data/schema" schemaExtension :: String schemaExtension = ".json" -columnHeadings :: [T.Text] -columnHeadings = +propertyColumnHeadings :: [T.Text] +propertyColumnHeadings = [ "Key" , "Default?" , "Type" , "Description" ] +listColumnHeadings :: [T.Text] +listColumnHeadings = + [ "Index" + , "Type" + , "Description" + ] + makeTitleMap :: [SchemaData] -> Map SchemaIdReference T.Text makeTitleMap = M.fromList . map (fromFilePath . schemaPath &&& title . schemaContent) makePandocTable :: Map SchemaIdReference T.Text -> SchemaData -> Pandoc -makePandocTable titleMap (SchemaData _ (ToplevelSchema theTitle theDescription schm _) parsedFooters) = +makePandocTable titleMap (SchemaData _ (ToplevelSchema theTitle theDescription _schema theMembers _) parsedFooters) = setTitle (text "JSON Schema for Scenarios") $ doc (header 3 (text theTitle)) <> theDescription - <> doc myTable + <> maybe mempty mkTable theMembers <> mconcat parsedFooters where - genRow :: (T.Text, SwarmSchema) -> [Blocks] - genRow (k, x) = - [ plain $ code k - , maybe mempty (plain . code . renderValue) $ defaultValue x - , plain . listToText titleMap $ schemaType x + renderItems someStuff = case someStuff of + ItemType x -> plain $ text "List of " <> listToText titleMap (schemaType x) + ItemList xs -> + makePropsTable False listColumnHeadings titleMap + . M.fromList + $ zip (map tshow [0 :: Int ..]) xs + + mkTable x = doc $ case x of + ObjectProperties props -> makePropsTable True propertyColumnHeadings titleMap props + ListMembers someStuff -> renderItems someStuff + SimpleType _ -> plain $ text "Simple type" -- FIXME not used yet + +genPropsRow :: Bool -> Map SchemaIdReference T.Text -> (T.Text, SwarmSchema) -> [Blocks] +genPropsRow includeDefaultColumn titleMap (k, x) = + firstColumn : applyWhen includeDefaultColumn (defaultColumn :) tailColumns + where + firstColumn = plain $ code k + defaultColumn = maybe mempty (plain . code . renderValue) $ defaultValue x + tailColumns = + [ plain . listToText titleMap $ schemaType x , fromList $ maybe [] (query id) $ objectDescription x ] - headerRow = map (plain . text) columnHeadings - myTable = simpleTable headerRow . map genRow . M.toList . fromMaybe mempty $ properties schm +makePropsTable :: + Bool -> + [T.Text] -> + Map SchemaIdReference T.Text -> + Map T.Text SwarmSchema -> + Blocks +makePropsTable includeDefaultColumn headingsList titleMap = + simpleTable headerRow . map (genPropsRow includeDefaultColumn titleMap) . M.toList + where + headerRow = map (plain . text) headingsList type FileStemAndExtension = (FilePath, String) @@ -84,7 +119,7 @@ genMarkdown schemaThings = pd = mconcat $ map (makePandocTable titleMap) $ - mkSchemaGraph (fromFilePath "scenario") schemaThings + sortAndPruneSchemas (fromFilePath "scenario") schemaThings parseSchemaFile :: FileStemAndExtension -> IO (Either T.Text ToplevelSchema) parseSchemaFile stemAndExtension = @@ -123,8 +158,36 @@ genScenarioSchemaDocs = do renderValue :: Value -> T.Text renderValue = \case Object obj -> showT obj - Array arr -> showT arr + Array arr -> brackets . T.intercalate ", " . map renderValue $ V.toList arr String t -> quote t - Number num -> showT num + Number num -> T.pack $ formatNumberCompact num Bool b -> showT b Null -> "null" + +fragmentHref :: Map SchemaIdReference T.Text -> SchemaIdReference -> T.Text +fragmentHref titleMap r@(SchemaIdReference ref) = + T.cons '#' . T.toLower . T.replace " " "-" $ x + where + x = M.findWithDefault ref r titleMap + +listToText :: Map SchemaIdReference T.Text -> SchemaType -> Inlines +listToText titleMap = \case + Simple xs -> renderAlternatives $ map code $ getList xs + Alternatives xs -> renderAlternatives $ map (listToText titleMap) xs + Reference r@(SchemaIdReference x) -> schemaLink r x + ListOf x -> text "[" <> listToText titleMap x <> text "]" + where + renderAlternatives = mconcat . intersperse (text " or ") + schemaLink r = link (fragmentHref titleMap r) "Link to object properties" . text + +-- | +-- Strips trailing zeros and decimal point from a floating-point number +-- when possible. +-- +-- Obtained from here: https://stackoverflow.com/a/35980995/105137 +formatNumberCompact :: Scientific -> String +formatNumberCompact v + | v == 0 = "0" + | abs v < 1e-5 || abs v > 1e10 = formatScientific Exponent Nothing v + | v - fromIntegral (floor v :: Integer) == 0 = formatScientific Fixed (Just 0) v + | otherwise = formatScientific Generic Nothing v diff --git a/src/Swarm/Doc/Schema/SchemaType.hs b/src/Swarm/Doc/Schema/SchemaType.hs new file mode 100644 index 0000000000..e6d8cd3b7a --- /dev/null +++ b/src/Swarm/Doc/Schema/SchemaType.hs @@ -0,0 +1,36 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.Doc.Schema.SchemaType where + +import Control.Applicative ((<|>)) +import Data.Aeson +import Data.Text (Text) +import Data.Text qualified as T +import System.FilePath (takeBaseName) + +newtype SingleOrList a = SingleOrList + { getList :: [a] + } + deriving (Eq, Ord, Show) + +instance (FromJSON a) => FromJSON (SingleOrList a) where + parseJSON x = + fmap SingleOrList $ + pure <$> parseJSON x <|> parseJSON x + +data SchemaType + = -- | A basic built-in type + Simple (SingleOrList Text) + | -- | Any one of multiple possible schema types + Alternatives [SchemaType] + | -- | A reference to a schema defined elsewhere + Reference SchemaIdReference + | -- | Members of a list, all of the given schema type + ListOf SchemaType + deriving (Eq, Ord, Show) + +newtype SchemaIdReference = SchemaIdReference Text + deriving (Eq, Ord, Show) + +fromFilePath :: FilePath -> SchemaIdReference +fromFilePath = SchemaIdReference . T.pack . takeBaseName diff --git a/src/Swarm/Doc/Schema/Superset.hs b/src/Swarm/Doc/Schema/Superset.hs new file mode 100644 index 0000000000..4da4404117 --- /dev/null +++ b/src/Swarm/Doc/Schema/Superset.hs @@ -0,0 +1,21 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Surface-level parsing of a JSON Schema +module Swarm.Doc.Schema.Superset where + +import Control.Applicative ((<|>)) +import Data.Aeson + +data ItemDescription a + = ItemList [a] + | ItemType a + deriving (Eq, Ord, Show) + +instance (FromJSON a) => FromJSON (ItemDescription a) where + parseJSON x = + -- TODO Which ordering is preferred? + -- ItemType <$> parseJSON x + -- <|> ItemList <$> parseJSON x + ItemList <$> parseJSON x + <|> ItemType <$> parseJSON x diff --git a/src/Swarm/Doc/Util.hs b/src/Swarm/Doc/Util.hs new file mode 100644 index 0000000000..005457dfae --- /dev/null +++ b/src/Swarm/Doc/Util.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Utilities for generating doc markup +module Swarm.Doc.Util where + +import Data.Text (Text) +import Data.Text qualified as T + +wrap :: Char -> Text -> Text +wrap c = T.cons c . flip T.snoc c + +codeQuote :: Text -> Text +codeQuote = wrap '`' + +addLink :: Text -> Text -> Text +addLink l t = T.concat ["[", t, "](", l, ")"] + +tshow :: (Show a) => a -> Text +tshow = T.pack . show diff --git a/swarm.cabal b/swarm.cabal index 2080e7bd1a..3a1b1da9eb 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -107,6 +107,9 @@ library Swarm.Doc.Schema.Parse Swarm.Doc.Schema.Refined Swarm.Doc.Schema.Render + Swarm.Doc.Schema.SchemaType + Swarm.Doc.Schema.Superset + Swarm.Doc.Util Swarm.Game.Failure Swarm.Game.Achievement.Attainment Swarm.Game.Achievement.Definitions @@ -276,6 +279,7 @@ library parser-combinators >= 1.2 && < 1.4, prettyprinter >= 1.7.0 && < 1.8, random >= 1.2.0 && < 1.3, + scientific >= 0.3.6 && < 0.3.8, servant >= 0.19 && < 0.21, servant-docs >= 0.12 && < 0.14, servant-server >= 0.19 && < 0.21,