walint: lint & adjust workadventure maps
Overview & Components
walint
is intended as a linter for workadventure maps that checks for common
errors (such as non-existent map entrypoints or missing asset files) and makes
suggestions to improve accessability.
It can also adjust maps — e.g. to automatically insert property values or help
enforce an event's map policies (among other things, this was used to resolve
special inter-assembly world://
links at rc3).
walint-mapserver
is a minimal implementation of a server that periodically
fetches, lints, and adjusts maps from a set of git repositories, writing them
to a path that can then be served as static map files for a workadventure
deployment. It can be used as a (very simple) replacement for rc3's hub and
mapservice at smaller events — to get started, manually list all map
repositories in config.toml
, then visit localhost:8080/admin/overview
.
Installing
From the CI pipeline
Gitlab automatically builds a version
of walint
each time something is pushed to the version of this repository
kept at the CCCV infra. The resulting binary should work fine on most linux
systems, especially if they're vaguely debian-like.
In case you get an incomprehensible or confusing error when executing it, try
running ldd walint
and see if anything is marked as not found, then install
it.
Build using stack
This uses a lockfile to pin versions of dependencies (as well as ghc
, the
haskell compiler). You will need
the haskell stack.
Run
stack build
If you lack ghc
and don't know how to install it, you can add --install-ghc
,
and stack
will take care of that for you (note that on NixOS, stack
may
use a fitting ghc
derivation if it finds one, even without --install-ghc
).
To install into your $PATH
, use
stack install
Alternatively, run walint
via stack:
stack run -- walint [options as normal]
Build using cabal
You can, but probably should not. Beware of older Aeson versions!
Usage
walint --config-file config.json --repository path \
[--out path] [--json] [--pretty] [--entrypoint main.json]
Options
-
--repository
: path to a map repository -
--entrypoint
: entrypoint of a map repository, i.e. a tiled map in its root directory.walint
will lint all maps reachable from it. If not given, it defaults tomain.json
-
--lintLevel
: limit output only to messages that have at most the given level. Valid levels areInfo
,Suggestion
,Warning
,Forbidden
,Error
,Fatal
. Defaults toSuggestion
. -
--json
: print output as json instead of the default human-friendly format -
--pretty
: if used with--json
, insert line breaks and indentation to make the output more readable. Otherwise no effect. -
--out path
: write the linted & adjusted repository to the given path. Any json written to this path will be minimised, and only those maps and assets which are reachable from the entrypoint will be writen at all. If not given,walint
will only run the linter and do nothing else. Ifwalint
encounters any references to local map assets which are not present in the repository, or if the map generates lints above the maximum lint level, it will instead exit with code 1 without touching the output path at all. -
--config-file file
: path to a configuation file. Required (for now). -
--config json
: takes a string which should contain a json object conforming to the same schema as the configuration file, except that all keys are optional. Keys given here will override whatever values are set in the configuration file.
Configuation
Take a look at config.json
for an example. Most keys are required, and do not
have default values. In config.json
, all possible keys are given.
Most options should be reasonably self-explanatory. Note that MaxLintLevel
differs from the option --lintLevel
: the latter merely determines what is
printed (in case json output is not enabled), the former determines the
maximum lint level allowed before the linter rejects the map and will refuse to
copy it to the path given as --out
.
Uri Schemas
walint
supports (basic) rewriting of URIs contained in the map json via the
UriSchemas
option, which maps schemas to rules describing what to do with such
links, depending on the scope in which they appear.
walint
takes a very reductive view or URIs: schema://domain/tail
Rewrite Rules
For now there are four types of such rules:
-
schema: {"scope":[scopes]}
: if in a scope listed inscopes
, allow any links of the givenschema
-
schema: {"scope":[scopes], "allowed":[allowed]}
: if in a scope listed inscopes
, only allow URIs whose domain occurs inallowed
. -
schema: {"scope":[scopes], "allowed":[allowed], "blocked":[blocked], "prefix":prefix}
: if in a scope listed inscopes
, allow URIs whose domain occurs inallowed
, disallow all whose domain occurs inblocked
, and for all others, prefix with the string given asprefix
-
schema: {"scope":[scopes], "substs":{domain: prefix, ...}}
: if in a scope listed inscopes
and given a URI with the domaindomain
, concatenateprefix
with the tail of this URI.
In case an URI is encountered and there is no applicable rule, it will be
rejected (note that this means you'll have to explicitly allow https://
for
links!)
There are currently four scopes:
-
map
applies to tiled map links (i.e.exitUrl
) -
website
toopenWebsite
-
audio
toplayAudio
-
script
to scripts
Output
By default walint
prints lints in a (hopefully) human-readable format. Its exit
code will be 1 if the maximum lint level set in its config was exceeded, and 0
otherwise. Only in the latter case will it write out an adjusted map respository
to the path passed to --out
. If the path given to --out
already exists,
walint
will print its normal output but refuse to write anything to that path,
and exit with code 2.
If the --json
option is given, output to stdout will be printed as json, which
should conform to the following schema (here defined in a quasi-haskell syntax):
-- | The main output type. All json output will conform to it.
type Output =
{ severity :: Level
-- ^ the maximum Lint level that occurred
, badges :: List Badge
-- ^ a list of badges occurring in any of the maps
, result :: Result
-- ^ detailed lints in a structured way
, resultText :: String
-- ^ all lints in a human-readable text block
}
-- | A detailed description of which errors occurred
type Result =
{ mapLints :: Map FilePath MapLint
-- ^ an object of per-map lints. Each key is a filepath from the repository's root
, missingAssets :: List Asset
-- ^ a list of missing assets
, missingDeps :: List Entrypoint
-- ^ a list of other missing dependencies (for now, just entrypoints)
}
-- | An object containing map lints
type MapLint =
{ general :: List Lint
-- ^ general lints (most often empty)
, layer :: Map Message Where
-- ^ an object of per-layer lints. Each key is a lint message
, tileset :: Map Message Where
-- ^ an object of per-tileset lints. Again, each key is a lint message
}
-- | Further desription of a single lint
type Where =
{ in :: List String
-- ^ where did this lint occur? (list of layers / tileset names)
, level :: Level
-- ^ what is this lint's level?
}
-- | Valid lint levels. Encoded as strings, listed in ascending order here.
data Level = Info | Suggestion | Warning | Forbidden | Error | Fatal
-- | description of a single (missing) asset
type Asset =
{ asset :: FilePath
-- ^ the filename, as referenced somewhere within the repository
, neededBy :: List FilePath
-- ^ list of filenames of maps which reference this asset
}
-- | description of a single (missing) entrypoint
type Entrypoint =
{ entrypoint :: String
-- ^ the entrypoint, as a string, e.g. path/to/map#entrypoint
, neededBy :: List FilePath
-- ^ list of filenames of maps which reference this entrypoint
}
-- | Lints that don't come grouped by place (for now, just those in generalLints)
type Lint =
{ level :: Level
-- ^ this lint's level
, msg :: String
-- ^ a human-readable (single-line) message
}
type Badge =
{ type :: AreaType
-- ^ type of the badge's area
, token :: String
-- ^ this badge's token
, x :: Number
-- ^ x position on the map
, y :: Number
-- ^ y position on the map
, width :: Maybe Number
-- ^ width of the rectangle/ellipse (not present if type=point)
, height :: Maybe Number
-- ^ height of the rectangle/ellipse (not present if type=point)
}
-- | types of "areas" for badges, encoded as lower-cased strings
data AreaType = Point | Rectangle | Ellipse