diff --git a/app/Main.hs b/app/Main.hs index 62d02c6077..21447624e7 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -116,6 +116,7 @@ cliParser = , Just Recipes <$ switch (long "recipes" <> help "Generate recipes page (uses data from recipes.yaml)") , Just Capabilities <$ switch (long "capabilities" <> help "Generate capabilities page (uses entity map)") , Just Commands <$ switch (long "commands" <> help "Generate commands page (uses constInfo, constCaps and inferConst)") + , Just Scenario <$ switch (long "scenario" <> help "Generate scenario schema page") ] seed :: Parser (Maybe Int) seed = optional $ option auto (long "seed" <> short 's' <> metavar "INT" <> help "Seed to use for world generation") diff --git a/data/scenarios/README_NEW.md b/data/scenarios/README_NEW.md new file mode 100644 index 0000000000..639c17e6f6 --- /dev/null +++ b/data/scenarios/README_NEW.md @@ -0,0 +1,206 @@ +### Scenario.json + +Scenario for the swarm game + +| Key | Default? | Type | Description | +|----------------|----------|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `attrs` | | `array` | A list of local attribute definitions | +| `author` | | `string` | The author of the scenario (optional). Typically this is a person's name, but it can be any string. It is displayed under the scenario description in the new game menu. | +| `creative` | `False` | `boolean` | Whether the scenario should start out in creative mode. | +| `description` | | `string` | A short description of the scenario. This shows up next to the new game menu when the scenario is selected. | +| `entities` | `[]` | [Object schema](#entities "Link to object properties") | An optional list of custom entities, to be used in addition to the built-in entities. See description of Entities. | +| `known` | `[]` | `array` | A list of names of standard or custom entities which should have the Known property added to them; that is, robots should know what they are without having to scan them. | +| `name` | | `string` | The name of the scenario. For official scenarios, this is what shows up in the new game menu. | +| `objectives` | `[]` | `array` | An optional list of objectives, aka winning conditions. The player has to complete the objectives in sequence to win. See the description of Objectives. | +| `recipes` | `[]` | [Object schema](#recipes "Link to object properties") | An optional list of custom recipes, to be used in addition to the built-in recipes. They can refer to built-in entities as well as custom entities. See description of Recipes. | +| `robots` | | `array` | A list of robots that will inhabit the world. See the description of Robots. | +| `seed` | | `number` | An optional seed that will be used to seed the random number generator. If a procedurally generated world is used, the seed hence determines the world. Hence, if the seed is specified, the procedurally generated world will be exactly the same every time, for every player. If omitted, a random seed will be used every time the scenario is loaded. | +| `solution` | | `string` | The (optional) text of a Swarm program that, when run on the base robot, completes all the objectives. For scenarios which are officially part of the Swarm repository, such a solution will be tested as part of CI testing. For scenarios loaded directly from a file, any provided solution is simply ignored. | +| `stepsPerTick` | | `number` | When present, this specifies the maximum number of CESK machine steps each robot is allowed to take per game tick. It is rather obscure and technical and only used in a few automated tests; most scenario authors should not need this. | +| `structures` | | `array` | Structure definitions | +| `subworlds` | | `array` | A list of subworld definitions | +| `version` | | `number` | The version number of the scenario schema. Currently, this should always be 1. | +| `world` | | [Object schema](#world "Link to object properties") | | + +### Entities.json + +Description of entities in the Swarm game + +| Key | Default? | Type | Description | +|-----|----------|------|-------------| + +### Explicit-waypoint.json + +Explicit waypoint definition + +| Key | Default? | Type | Description | +|--------|----------|----------------------------------------------------------|---------------| +| `loc` | | [Object schema](#planar-loc "Link to object properties") | | +| `name` | | `string` | Waypoint name | + +### Recipes.json + +How to make (or drill) entities in the Swarm game + +| Key | Default? | Type | Description | +|-----|----------|------|-------------| + +### Combustion.json + +Properties of combustion + +| Key | Default? | Type | Description | +|------------|----------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `duration` | | `array` | 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 | null` | What entity, if any, is left over after combustion | + +### Planar-loc.json + +x and y coordinates of a location in a particular world + +| Key | Default? | Type | Description | +|-----|----------|------|-------------| + +### Cosmic-loc.json + +Planar location plus subworld + +| Key | Default? | Type | Description | +|------------|----------|----------------------------------------------------------|------------------| +| `loc` | | [Object schema](#planar-loc "Link to object properties") | | +| `subworld` | | `string` | Name of subworld | + +### Portal.json + +Portal definition + +| Key | Default? | Type | Description | +|--------------|----------|-----------|-----------------------------------------------------------| +| `consistent` | | `boolean` | Whether this portal is spatially consistent across worlds | +| `entrance` | | `string` | Name of entrance waypoint | +| `exitInfo` | | `object` | Exit definition | +| `reorient` | | `string` | Passing through this portal changes a robot's orientation | + +### Display.json + +A display specifies how an entity or a robot (robots are essentially +special kinds of entities) is displayed in the world. It consists of a +key-value mapping described by the following table. + +| Key | Default? | Type | Description | +|------------------|---------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `attr` | `"entity"` | `string` | The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found at https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/View/Attribute/Attr.hs. | +| `char` | `" "` | `string` | The default character that should be used to draw the robot or entity. | +| `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 []` | `object` | Currently unused | +| `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. | + +### Objective.json + +The top-level objectives field contains a list of objectives that must +be completed in sequence. Each objective has a goal description and a +condition. + +| Key | Default? | Type | Description | +|----------------|----------|-----------|---------------------------------------------------------------------------------------------------| +| `condition` | | `string` | A swarm program that will be hypothetically run each tick to check if the condition is fulfilled. | +| `goal` | | `array` | The goal description as a list of paragraphs that the player can read. | +| `hidden` | | `boolean` | Whether this goal should be suppressed from the Goals dialog prior to achieving it | +| `id` | | `string` | A short identifier for referencing as a prerequisite | +| `optional` | | `boolean` | Whether completion of this objective is required to achieve a 'Win' of the scenario | +| `prerequisite` | | `object` | | +| `teaser` | | `string` | A compact (2-3 word) summary of the goal | + +### Inventory.json + +A list of \[count, entity name\] pairs, specifying the number of each +entity. + +| Key | Default? | Type | Description | +|-----|----------|------|-------------| + +### Attribute.json + +Local attribute definitions + +| Key | Default? | Type | Description | +|---------|----------|----------|-----------------------| +| `bg` | | `string` | Background color | +| `fg` | | `string` | Foreground color | +| `name` | | `string` | Name of attribute | +| `style` | | `array` | Style properties list | + +### Entity.json + +Description of an entity in the Swarm game + +| Key | Default? | Type | Description | +|----------------|----------|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `capabilities` | `"[]"` | `array` | A list of capabilities provided by entity, when it is equipped as a device. See Capabilities. | +| `combustion` | | [Object schema](#combustion "Link to object properties") | Properties of combustion. | +| `description` | | `array` | A description of the entity, as a list of paragraphs. | +| `display` | | [Object schema](#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. | +| `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. | +| `properties` | `"[]"` | `array` | A list of properties of this entity. See Entity properties. | +| `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. | + +### Placement.json + +Structure placement + +| Key | Default? | Type | Description | +|----------|----------|----------------------------------------------------------|------------------------------| +| `offset` | | [Object schema](#planar-loc "Link to object properties") | | +| `orient` | | `object` | Orientation of structure | +| `src` | | `string` | Name of structure definition | + +### World.json + +Description of the world in the Swarm game + +| Key | Default? | Type | Description | +|--------------|---------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | | `array` | 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 https://github.com/swarm-game/swarm/blob/main/src/Swarm/Game/WorldGen.hs#L204 . | +| `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` | | `array` | Structure placements | +| `portals` | | `array` | A list of portal definitions that reference waypoints. | +| `scrollable` | `True` | `boolean` | Whether players are allowed to scroll the world map. | +| `structures` | | `array` | 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` | | `array` | Single-location waypoint definitions | + +### Structure.json + +Structure definitions + +| Key | Default? | Type | Description | +|-------------|----------|----------|---------------------------| +| `name` | | `string` | Name of this substructure | +| `structure` | | `object` | Structure properties | + +### Robot.json + +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` | `[]` | `array` | 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"` | [Object schema](#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` | `[]` | [Object schema](#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` | | `Object (fromList [("$ref",String "./cosmic-loc.json")]) | Object (fromList [("$ref",String "./planar-loc.json")])` | 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` | `[]` | `array` | A list of entities that this robot cannot walk across. | diff --git a/data/schema/display.json b/data/schema/display.json index e25f6d4c5d..ed2e56829f 100644 --- a/data/schema/display.json +++ b/data/schema/display.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/display.json", "title": "Swarm entity display", - "description": "How to display an entity or robot in the Swarm game", + "description": "A display specifies how an entity or a robot (robots are essentially special kinds of entities) is displayed in the world. It consists of a key-value mapping described by the following table.", "type": "object", "additionalProperties": false, "properties": { @@ -47,7 +47,7 @@ "blue", "water" ], - "description": "The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found at https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/Attr.hs." + "description": "The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found at https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/View/Attribute/Attr.hs." }, "priority": { "default": 1, diff --git a/data/schema/entities.json b/data/schema/entities.json index fbeb8677fa..192fae2cef 100644 --- a/data/schema/entities.json +++ b/data/schema/entities.json @@ -5,106 +5,6 @@ "description": "Description of entities in the Swarm game", "type": "array", "items": { - "description": "Description of an entity in the Swarm game", - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The name of the entity. This is what will show up in the inventory and how the entity can be referred to." - }, - "display": { - "type": "object", - "$ref": "./display.json", - "description": "Display information for the entity." - }, - "plural": { - "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." - }, - "description": { - "type": "array", - "items": [ - { - "type": "string" - } - ], - "description": "A description of the entity, as a list of paragraphs." - }, - "orientation": { - "default": "null", - "type": "array", - "items": [ - { - "name": "X coordinate", - "type": "number" - }, - { - "name": "Y coordinate", - "type": "number" - } - ], - "description": "A 2-tuple of integers specifying an orientation vector for the entity. Currently unused." - }, - "growth": { - "default": "null", - "type": "array", - "items": [ - { - "name": "minimum", - "type": "number" - }, - { - "name": "maximum", - "type": "number" - } - ], - "description": "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." - }, - "combustion": { - "type": "object", - "$ref": "./combustion.json", - "description": "Properties of combustion." - }, - "yields": { - "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." - }, - "properties": { - "default": "[]", - "type": "array", - "items": [ - { - "type": "string", - "examples": [ - "unwalkable", - "portable", - "infinite", - "known", - "growable" - ] - } - ], - "description": "A list of properties of this entity. See Entity properties." - }, - "capabilities": { - "default": "[]", - "type": "array", - "items": [ - { - "type": "string" - } - ], - "description": "A list of capabilities provided by entity, when it is equipped as a device. See Capabilities." - } - }, - "required": [ - "name", - "display", - "description" - ] + "$ref": "./entity.json" } - } diff --git a/data/schema/entity.json b/data/schema/entity.json new file mode 100644 index 0000000000..b50c47c7f2 --- /dev/null +++ b/data/schema/entity.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entity.json", + "title": "Entity", + "description": "Description of an entity in the Swarm game", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the entity. This is what will show up in the inventory and how the entity can be referred to." + }, + "display": { + "type": "object", + "$ref": "./display.json", + "description": "Display information for the entity." + }, + "plural": { + "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." + }, + "description": { + "type": "array", + "items": [ + { + "type": "string" + } + ], + "description": "A description of the entity, as a list of paragraphs." + }, + "orientation": { + "default": "null", + "type": "array", + "items": [ + { + "name": "X coordinate", + "type": "number" + }, + { + "name": "Y coordinate", + "type": "number" + } + ], + "description": "A 2-tuple of integers specifying an orientation vector for the entity. Currently unused." + }, + "growth": { + "default": "null", + "type": "array", + "items": [ + { + "name": "minimum", + "type": "number" + }, + { + "name": "maximum", + "type": "number" + } + ], + "description": "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." + }, + "combustion": { + "type": "object", + "$ref": "./combustion.json", + "description": "Properties of combustion." + }, + "yields": { + "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." + }, + "properties": { + "default": "[]", + "type": "array", + "items": [ + { + "type": "string", + "examples": [ + "unwalkable", + "portable", + "infinite", + "known", + "growable" + ] + } + ], + "description": "A list of properties of this entity. See Entity properties." + }, + "capabilities": { + "default": "[]", + "type": "array", + "items": [ + { + "type": "string" + } + ], + "description": "A list of capabilities provided by entity, when it is equipped as a device. See Capabilities." + } + }, + "required": [ + "name", + "display", + "description" + ] +} diff --git a/data/schema/objective.json b/data/schema/objective.json index ccc8c5dc50..af297f0415 100644 --- a/data/schema/objective.json +++ b/data/schema/objective.json @@ -35,6 +35,8 @@ "description": "A compact (2-3 word) summary of the goal", "type": "string" }, - "prerequisite": {} + "prerequisite": { + "type": "object" + } } } diff --git a/src/Swarm/Doc/Gen.hs b/src/Swarm/Doc/Gen.hs index 6feb33a1c8..394b92f4af 100644 --- a/src/Swarm/Doc/Gen.hs +++ b/src/Swarm/Doc/Gen.hs @@ -42,6 +42,7 @@ import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Tuple (swap) import Swarm.Doc.Pedagogy +import Swarm.Doc.Schema.Scenario import Swarm.Game.Display (displayChar) import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E @@ -92,7 +93,7 @@ data EditorType = Emacs | VSCode | Vim deriving (Eq, Show, Enum, Bounded) -- | An enumeration of the kinds of cheat sheets we can produce. -data SheetType = Entities | Commands | Capabilities | Recipes +data SheetType = Entities | Commands | Capabilities | Recipes | Scenario deriving (Eq, Show, Enum, Bounded) -- | A configuration record holding the URLs of the various cheat @@ -135,6 +136,7 @@ generateDocs = \case entities <- loadEntities recipes <- loadRecipes entities sendIO $ T.putStrLn $ recipePage address recipes + Scenario -> genScenarioSchemaDocs TutorialCoverage -> renderTutorialProgression >>= putStrLn . T.unpack -- ---------------------------------------------------------------------------- diff --git a/src/Swarm/Doc/Schema/Refined.hs b/src/Swarm/Doc/Schema/Refined.hs new file mode 100644 index 0000000000..48947ba093 --- /dev/null +++ b/src/Swarm/Doc/Schema/Refined.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Refined JSON schema after converting +-- all JSON Value types to their specific sum types +module Swarm.Doc.Schema.Refined where + +import Data.Aeson +import Control.Applicative ((<|>)) +import Data.Text (Text) +import Text.Pandoc.Builder +import Data.Text qualified as T + +newtype SingleOrList a = SingleOrList { + getList :: [a] + } deriving (Eq, Ord, Show) + +instance (FromJSON a) => FromJSON (SingleOrList a) where + parseJSON x = SingleOrList <$> do + (pure <$> parseJSON x) <|> parseJSON x + +type SchemaIdReference = Text + +data SchemaType = + Simple (SingleOrList Text) + | Reference SchemaIdReference + | Alternates [Value] + deriving (Eq, Ord, Show) + +fragmentHref :: SchemaIdReference -> Text +fragmentHref = T.cons '#' . T.filter (/= '.'). T.toLower + +listToText :: SchemaType -> Inlines +listToText = \case + Simple xs -> code $ T.intercalate " | " $ getList xs + Reference x -> link (fragmentHref x) "Link to object properties" $ text $ "Object schema" + Alternates xs -> code $ T.intercalate " | " $ map (T.pack . show) xs \ No newline at end of file diff --git a/src/Swarm/Doc/Schema/Scenario.hs b/src/Swarm/Doc/Schema/Scenario.hs new file mode 100644 index 0000000000..b21640c4d6 --- /dev/null +++ b/src/Swarm/Doc/Schema/Scenario.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Render a markdown document fragment +-- from the Scenario JSON schema files. +module Swarm.Doc.Schema.Scenario where + +import Control.Arrow (left, (&&&)) +import Data.Aeson +import Data.Map.Strict qualified as M +import Swarm.Doc.Schema.Refined +import Data.Maybe (fromMaybe) +import Data.Text qualified as T +import Swarm.Doc.Schema.Surface +import Swarm.Util (quote, showT) +import System.FilePath ((<.>), (), splitExtension, takeBaseName) +import Text.Pandoc +import Text.Pandoc.Builder +import System.Directory (listDirectory) + +scenariosDir :: FilePath +scenariosDir = "data/scenarios" + +schemasDir :: FilePath +schemasDir = "data/schema" + +schemaExtension :: String +schemaExtension = ".json" + +columnHeadings :: [T.Text] +columnHeadings = + [ "Key" + , "Default?" + , "Type" + , "Description" + ] + +makePandocTable :: (String, SwarmSchema) -> Pandoc +makePandocTable (fn, schm) = + setTitle (text "JSON Schema for Scenarios") $ + doc $ + header 3 (text . T.toTitle . T.pack $ takeBaseName fn) + <> maybe mempty (para . text) (description schm) + <> myTable + where + genRow :: (T.Text, SwarmSchema) -> [Blocks] + genRow (k, x) = + [ plain $ code k + , maybe mempty (plain . code . renderValue) $ defaultValue x + , plain . listToText $ schemaType x + , plain . text . fromMaybe "" $ description x + ] + + headerRow = map (plain . text) columnHeadings + myTable = simpleTable headerRow . map genRow . M.toList . fromMaybe mempty $ properties schm + +type FileStemAndExtension = (FilePath, String) + +recombineExtension :: FileStemAndExtension -> FilePath +recombineExtension (filenameStem, fileExtension) = + filenameStem <.> fileExtension + +genScenarioSchemaDocs :: IO () +genScenarioSchemaDocs = do + dirContents <- listDirectory schemasDir + let inputFiles = filter ((== schemaExtension) . snd) $ map splitExtension dirContents + xs <- mapM (sequenceA . (recombineExtension &&& parseSchemaFile)) inputFiles + let eitherMarkdown = do + schemas <- traverse sequenceA xs + let pd = mconcat $ map makePandocTable schemas + left renderError $ runPure (writeMarkdown (def {writerExtensions = extensionsFromList [Ext_pipe_tables]}) pd) + + case eitherMarkdown of + Left e -> print $ unwords ["Failed:", T.unpack e] + Right md -> writeFile (scenariosDir "README_NEW.md") $ T.unpack md + where + + parseSchemaFile :: FileStemAndExtension -> IO (Either T.Text SwarmSchema) + parseSchemaFile stemAndExtension = + left (prependPath . T.pack) <$> eitherDecodeFileStrict fullPath + where + prependPath = ((T.unwords ["in", quote (T.pack filename)] <> ": ") <>) + filename = recombineExtension stemAndExtension + fullPath = schemasDir filename + +renderValue :: Value -> T.Text +renderValue = \case + Object obj -> showT obj + Array arr -> showT arr + String t -> quote t + Number num -> showT num + Bool b -> showT b + Null -> "null" diff --git a/src/Swarm/Doc/Schema/Surface.hs b/src/Swarm/Doc/Schema/Surface.hs new file mode 100644 index 0000000000..3563d46890 --- /dev/null +++ b/src/Swarm/Doc/Schema/Surface.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE DuplicateRecordFields #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- There are no modern, comprehensive, JSON Schema parsing +-- libraries in Haskell, as explained in this post: +-- https://dev.to/sshine/a-review-of-json-schema-libraries-for-haskell-321 +-- +-- Therefore, a parser for a small, custom subset of JSON Schema is implemented here, +-- simply for rendering Markdown documentation from Swarm's schema. +module Swarm.Doc.Schema.Surface where + +import Data.Aeson +import Data.List.Extra (replace) +import Data.Map (Map) +import Data.Text (Text) +import Data.Text qualified as T +import Control.Applicative ((<|>)) +import GHC.Generics (Generic) +import System.FilePath (takeBaseName) +import Swarm.Doc.Schema.Refined + +schemaJsonOptions :: Options +schemaJsonOptions = + defaultOptions + { fieldLabelModifier = replace "S" "$" . tail -- drops leading underscore + } + +data SchemaRaw = SchemaRaw + { _description :: Maybe Text + , _default :: Maybe Value + , _title :: Maybe Text + , _type :: Maybe (SingleOrList Text) + , _properties :: Maybe (Map Text SwarmSchema) + , _items :: Maybe Value + , _examples :: Maybe [Value] + , _Sref :: Maybe Text + , _oneOf :: Maybe [Value] + } + 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 SwarmSchema = SwarmSchema { + schemaType :: SchemaType + , defaultValue :: Maybe Value + , items :: Maybe Value + , description :: Maybe Text + , properties :: Maybe (Map Text SwarmSchema) + , examples :: Maybe [Value] + } + deriving (Eq, Ord, Show) + +instance FromJSON SwarmSchema where + parseJSON x = do + + rawSchema :: rawSchema <- parseJSON x + let maybeType = + (Reference . T.pack . takeBaseName . T.unpack <$> _Sref rawSchema) + <|> (Simple <$> _type rawSchema) + <|> (Alternates <$> _oneOf rawSchema) + + theType <- maybe (fail "Unspecified sub-schema type") return maybeType + + return $ SwarmSchema { + schemaType = theType + , defaultValue = _default rawSchema + , items = _items rawSchema + , description = _description rawSchema + , examples = _examples rawSchema + , properties = _properties rawSchema + } \ No newline at end of file diff --git a/swarm.cabal b/swarm.cabal index caf5ce3bb3..d38743e408 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -103,6 +103,9 @@ library Swarm.Constant Swarm.Doc.Gen Swarm.Doc.Pedagogy + Swarm.Doc.Schema.Refined + Swarm.Doc.Schema.Scenario + Swarm.Doc.Schema.Surface Swarm.Game.Failure Swarm.Game.Achievement.Attainment Swarm.Game.Achievement.Definitions @@ -263,6 +266,8 @@ library minimorph >= 0.3 && < 0.4, transformers >= 0.5 && < 0.7, mtl >= 2.2.2 && < 2.4, + pandoc >= 3.0 && < 3.2, + pandoc-types >= 1.23 && < 1.24, murmur3 >= 1.0.4 && < 1.1, natural-sort >= 0.1.2 && < 0.2, palette >= 0.3 && < 0.4,