2021-11-07 17:17:48 +01:00
import ScriptUtils from "./ScriptUtils"
import { readFileSync , writeFileSync } from "fs"
2023-07-28 14:37:51 +02:00
import { JsonSchema } from "../src/UI/Studio/jsonSchema"
import { ConfigMeta } from "../src/UI/Studio/configMeta"
import { Utils } from "../src/Utils"
2023-08-23 11:11:53 +02:00
import Validators from "../src/UI/InputElement/Validators"
2023-10-06 23:56:50 +02:00
import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
2024-01-12 23:19:31 +01:00
import Constants from "../src/Models/Constants"
2022-01-31 00:39:54 +01:00
2023-06-23 16:14:43 +02:00
const metainfo = {
type : "One of the inputValidator types" ,
2023-10-21 09:35:54 +02:00
typeHelper : "Helper arguments for the type input, comma-separated. Same as 'args'" ,
2023-06-23 16:14:43 +02:00
types : "Is multiple types are allowed for this field, then first show a mapping to pick the appropriate subtype. `Types` should be `;`-separated and contain precisely the same amount of subtypes" ,
typesdefault : "Works in conjuction with `types`: this type will be selected by default" ,
group : "A kind of label. Items with the same group name will be placed in the same region" ,
default : "The default value which is used if no value is specified" ,
question : "The question to ask in the tagRenderingConfig" ,
iftrue : "For booleans only - text to show with 'yes'" ,
iffalse : "For booleans only - text to show with 'no'" ,
ifunset :
"Only applicable if _not_ a required item. This will appear in the 'not set'-option as extra description" ,
inline : "A text, containing `{value}`. This will be used as freeform rendering and will be included into the rendering" ,
2023-10-06 23:56:50 +02:00
suggestions :
'a javascript expression generating mappings; executed in an environment which has access to `layers: Map<string, LayerConfig>` and `themes: Map<string, ThemeConfig>`. Should return an array of type `{if: \'value=*\', then: string}[]`. Example: `return Array.from(layers.keys()).map(key => ({if: "value="+key, then: key+" - "+layers.get(key).description}))`. This code is executed at compile time, so no CSP is needed ' ,
title : "a title that is given to a MultiType" ,
multianswer : "set to 'true' if multiple options should be selectable" ,
2023-06-23 16:14:43 +02:00
}
2023-06-23 17:28:44 +02:00
2023-08-23 11:11:53 +02:00
/ * *
* Applies 'onEach' on every leaf of the JSONSchema
* @param onEach
* @param scheme
* @param fullScheme
* @param path
* @param isHandlingReference
* @param required
* @constructor
* /
2022-01-31 00:39:54 +01:00
function WalkScheme < T > (
2023-06-21 17:13:09 +02:00
onEach : ( schemePart : JsonSchema , path : string [ ] ) = > T ,
2022-01-31 00:39:54 +01:00
scheme : JsonSchema ,
fullScheme : JsonSchema & { definitions? : any } = undefined ,
path : string [ ] = [ ] ,
2023-06-18 00:44:57 +02:00
isHandlingReference = [ ] ,
2023-10-06 23:56:50 +02:00
required : string [ ] ,
skipRoot = false
2023-06-18 00:44:57 +02:00
) : { path : string [ ] ; required : boolean ; t : T } [ ] {
const results : { path : string [ ] ; required : boolean ; t : T } [ ] = [ ]
2022-01-31 00:39:54 +01:00
if ( scheme === undefined ) {
return [ ]
}
if ( scheme [ "$ref" ] !== undefined ) {
const ref = scheme [ "$ref" ]
const prefix = "#/definitions/"
if ( ! ref . startsWith ( prefix ) ) {
throw "References is not relative!"
}
const definitionName = ref . substr ( prefix . length )
if ( isHandlingReference . indexOf ( definitionName ) >= 0 ) {
2023-06-23 16:14:43 +02:00
// We abort here to avoid infinite recursion
2023-03-09 15:15:24 +01:00
return [ ]
2022-01-31 00:39:54 +01:00
}
2023-08-23 11:11:53 +02:00
// The 'scheme' might contain some extra info, such as 'description'
// This effectively overwrites properties from the loaded scheme
2022-01-31 00:39:54 +01:00
const loadedScheme = fullScheme . definitions [ definitionName ]
2023-08-23 11:11:53 +02:00
const syntheticScheme = { . . . loadedScheme , . . . scheme }
syntheticScheme [ "child-description" ] = loadedScheme . description
delete syntheticScheme [ "$ref" ]
2023-06-21 17:13:09 +02:00
return WalkScheme (
onEach ,
2023-08-23 11:11:53 +02:00
syntheticScheme ,
2023-06-21 17:13:09 +02:00
fullScheme ,
path ,
[ . . . isHandlingReference , definitionName ] ,
2023-10-06 23:56:50 +02:00
required ,
skipRoot
2023-06-21 17:13:09 +02:00
)
2022-01-31 00:39:54 +01:00
}
fullScheme = fullScheme ? ? scheme
2023-10-06 23:56:50 +02:00
if ( ! skipRoot ) {
let t = onEach ( scheme , path )
if ( t !== undefined ) {
const isRequired = required ? . indexOf ( path . at ( - 1 ) ) >= 0
results . push ( {
path ,
required : isRequired ,
t ,
} )
}
2022-02-09 22:34:02 +01:00
}
2022-02-28 17:17:38 +01:00
2023-10-06 23:56:50 +02:00
function walk ( v : JsonSchema , skipRoot = false ) {
2022-01-31 00:39:54 +01:00
if ( v === undefined ) {
return
}
2023-10-06 23:56:50 +02:00
results . push (
. . . WalkScheme ( onEach , v , fullScheme , path , isHandlingReference , v . required , skipRoot )
)
2022-01-31 00:39:54 +01:00
}
2023-10-06 23:56:50 +02:00
function walkEach ( scheme : JsonSchema [ ] , skipRoot : boolean = false ) {
2022-01-31 00:39:54 +01:00
if ( scheme === undefined ) {
return
}
2022-05-17 01:46:59 +02:00
2023-10-06 23:56:50 +02:00
scheme . forEach ( ( v ) = > walk ( v , skipRoot ) )
2022-01-31 00:39:54 +01:00
}
2022-02-09 22:34:02 +01:00
{
2022-02-28 17:17:38 +01:00
walkEach ( scheme . enum )
2023-10-06 23:56:50 +02:00
walkEach ( scheme . anyOf , true )
2022-05-17 01:46:59 +02:00
walkEach ( scheme . allOf )
2022-02-28 17:17:38 +01:00
if ( Array . isArray ( scheme . items ) ) {
2023-10-06 23:56:50 +02:00
// walk and walkEach are local functions which push to the result array
2022-02-28 17:17:38 +01:00
walkEach ( < any > scheme . items )
} else {
walk ( < any > scheme . items )
2022-01-31 00:39:54 +01:00
}
for ( const key in scheme . properties ) {
const prop = scheme . properties [ key ]
2022-02-28 17:17:38 +01:00
results . push (
2023-06-18 00:44:57 +02:00
. . . WalkScheme (
onEach ,
prop ,
fullScheme ,
[ . . . path , key ] ,
isHandlingReference ,
scheme . required
)
2022-09-08 21:40:48 +02:00
)
2022-01-31 00:39:54 +01:00
}
}
return results
}
2023-08-23 11:11:53 +02:00
function extractHintsFrom (
description : string ,
fieldnames : string [ ] ,
path : ( string | number ) [ ] ,
2023-10-30 13:45:44 +01:00
type : any ,
schemepart : any
2023-08-23 11:11:53 +02:00
) : Record < string , string > {
if ( ! description ) {
return { }
}
const hints = { }
const lines = description . split ( "\n" )
for ( const fieldname of fieldnames ) {
const hintIndex = lines . findIndex ( ( line ) = >
line
. trim ( )
. toLocaleLowerCase ( )
. startsWith ( fieldname + ":" )
)
if ( hintIndex < 0 ) {
continue
}
const hintLine = lines [ hintIndex ] . substring ( ( fieldname + ":" ) . length ) . trim ( )
if ( fieldname === "type" ) {
hints [ "typehint" ] = hintLine
} else {
hints [ fieldname ] = hintLine
}
}
if ( hints [ "types" ] ) {
2023-10-30 13:45:44 +01:00
const notRequired = hints [ "ifunset" ] !== undefined
2023-08-23 11:11:53 +02:00
const numberOfExpectedSubtypes = hints [ "types" ] . replaceAll ( "|" , ";" ) . split ( ";" ) . length
2023-10-30 13:45:44 +01:00
if ( ! Array . isArray ( type ) && ! notRequired ) {
2023-08-23 11:11:53 +02:00
throw (
"At " +
path . join ( "." ) +
"Invalid hint in the documentation: `types` indicates that there are " +
numberOfExpectedSubtypes +
" subtypes, but object does not support subtypes. Did you mean `type` instead?\n\tTypes are: " +
2023-10-30 13:45:44 +01:00
hints [ "types" ] +
"\n: hints: " +
JSON . stringify ( hints ) +
" req:" +
JSON . stringify ( schemepart )
2023-08-23 11:11:53 +02:00
)
}
const numberOfActualTypes = type . length
2023-10-30 13:45:44 +01:00
if (
numberOfActualTypes !== numberOfExpectedSubtypes &&
notRequired &&
numberOfActualTypes + 1 !== numberOfExpectedSubtypes
) {
2023-08-23 11:11:53 +02:00
throw ` At ${ path . join (
"."
) } \ nInvalid hint in the documentation : \ ` types \` indicates that there are ${ numberOfExpectedSubtypes } subtypes, but there are ${ numberOfActualTypes } subtypes
\ tTypes are : $ { hints [ "types" ] } `
}
}
if ( hints [ "suggestions" ] ) {
const suggestions = hints [ "suggestions" ]
2024-01-12 23:19:31 +01:00
const f = new Function ( "{ layers, themes, validators, Constants }" , suggestions )
2023-08-23 11:11:53 +02:00
hints [ "suggestions" ] = f ( {
layers : AllSharedLayers.sharedLayers ,
themes : AllKnownLayouts.allKnownLayouts ,
validators : Validators ,
2024-02-20 13:33:38 +01:00
Constants : Constants ,
2023-08-23 11:11:53 +02:00
} )
2024-04-23 21:42:35 +02:00
if ( hints [ "suggestions" ] ? . indexOf ( null ) >= 0 ) {
throw "A suggestion generated 'null' for " + path . join ( "." ) + ". Check the docstring, specifically 'suggestions'. Pay attention to double commas"
}
2023-08-23 11:11:53 +02:00
}
return hints
}
/ * *
* Extracts the 'configMeta' from the given schema , based on attributes in the description
* @param fieldnames
* @param fullSchema
* /
2023-06-23 16:14:43 +02:00
function addMetafields ( fieldnames : string [ ] , fullSchema : JsonSchema ) : ConfigMeta [ ] {
2023-08-23 11:11:53 +02:00
const fieldNamesSet = new Set ( fieldnames )
2023-06-21 17:13:09 +02:00
const onEach = ( schemePart , path ) = > {
2022-02-09 22:34:02 +01:00
if ( schemePart . description === undefined ) {
return
}
2022-02-28 17:17:38 +01:00
const type = schemePart . items ? . anyOf ? ? schemePart . type ? ? schemePart . anyOf
2023-08-23 11:11:53 +02:00
let description = schemePart . description
2023-10-30 13:45:44 +01:00
let hints = extractHintsFrom ( description , fieldnames , path , type , schemePart )
2023-08-23 11:11:53 +02:00
const childDescription = schemePart [ "child-description" ]
if ( childDescription ) {
2023-10-30 13:45:44 +01:00
const childHints = extractHintsFrom (
childDescription ,
fieldnames ,
path ,
type ,
schemePart
)
2023-08-23 11:11:53 +02:00
hints = { . . . childHints , . . . hints }
description = description ? ? childDescription
2023-06-16 02:36:11 +02:00
}
2023-08-23 11:11:53 +02:00
const cleanedDescription : string [ ] = [ ]
for ( const line of description . split ( "\n" ) ) {
const keyword = line . split ( ":" ) . at ( 0 ) . trim ( ) . toLowerCase ( )
if ( fieldNamesSet . has ( keyword ) ) {
continue
2023-06-21 17:13:09 +02:00
}
2023-08-23 11:11:53 +02:00
cleanedDescription . push ( line )
2023-06-21 17:13:09 +02:00
}
2023-08-23 11:11:53 +02:00
return {
hints ,
type ,
2023-10-06 23:56:50 +02:00
description : cleanedDescription.filter ( ( l ) = > l !== "" ) . join ( "\n" ) ,
2023-06-23 16:14:43 +02:00
}
2023-06-21 17:13:09 +02:00
}
2023-06-23 16:14:43 +02:00
return WalkScheme ( onEach , fullSchema , fullSchema , [ ] , [ ] , fullSchema . required ) . map (
( { path , required , t } ) = > ( { path , required , . . . t } )
)
2023-06-16 02:36:11 +02:00
}
2023-06-23 16:14:43 +02:00
function substituteReferences (
paths : ConfigMeta [ ] ,
origSchema : JsonSchema ,
allDefinitions : Record < string , JsonSchema >
) {
for ( const path of paths ) {
if ( ! Array . isArray ( path . type ) ) {
continue
}
2023-10-06 23:56:50 +02:00
2023-06-23 16:14:43 +02:00
for ( let i = 0 ; i < path . type . length ; i ++ ) {
const typeElement = path . type [ i ]
const ref = typeElement [ "$ref" ]
if ( ! ref ) {
continue
}
const name = ref . substring ( "#/definitions/" . length )
if ( name . startsWith ( "{" ) || name . startsWith ( "Record<" ) ) {
continue
}
if ( origSchema [ "definitions" ] ? . [ name ] ) {
path . type [ i ] = origSchema [ "definitions" ] ? . [ name ]
continue
}
2023-06-26 10:23:42 +02:00
if ( name === "DeleteConfigJson" || name === "TagRenderingConfigJson" ) {
2023-06-23 16:14:43 +02:00
const target = allDefinitions [ name ]
if ( ! target ) {
throw "Cannot expand reference for type " + name + "; it does not exist "
}
path . type [ i ] = target
}
}
}
}
2023-06-23 17:28:44 +02:00
function validateMeta ( path : ConfigMeta ) : string | undefined {
if ( path . path . length == 0 ) {
return
}
2023-10-06 23:56:50 +02:00
const ctx = "Definition for field '" + path . path . join ( "." ) + "'"
2023-06-23 17:28:44 +02:00
if ( path . hints . group === undefined && path . path . length == 1 ) {
return (
ctx +
" does not have a group set (but it is a top-level element which should have a group) "
)
}
if ( path . hints . group === "hidden" ) {
return undefined
}
if ( path . hints . typehint === "tag" ) {
return undefined
}
if ( path . path [ 0 ] == "mapRendering" || path . path [ 0 ] == "tagRenderings" ) {
return undefined
}
if ( path . hints . question === undefined && ! Array . isArray ( path . type ) ) {
2023-10-06 23:56:50 +02:00
/ * r e t u r n (
2023-10-30 13:45:44 +01:00
ctx +
" does not have a question set. As such, MapComplete-studio users will not be able to set this property"
) //*/
2023-06-23 17:28:44 +02:00
}
return undefined
}
function extractMeta (
typename : string ,
path : string ,
allDefinitions : Record < string , JsonSchema >
) : string [ ] {
2023-08-23 11:11:53 +02:00
const schema : JsonSchema = JSON . parse (
2023-06-16 02:36:11 +02:00
readFileSync ( "./Docs/Schemas/" + typename + ".schema.json" , { encoding : "utf8" } )
)
2023-08-23 11:11:53 +02:00
const metakeys = Array . from ( Object . keys ( metainfo ) ) . map ( ( s ) = > s . toLowerCase ( ) )
2023-06-16 02:36:11 +02:00
2023-08-23 11:11:53 +02:00
const paths = addMetafields ( metakeys , schema )
2023-06-23 16:14:43 +02:00
2023-08-23 11:11:53 +02:00
substituteReferences ( paths , schema , allDefinitions )
2022-02-09 22:34:02 +01:00
2023-08-23 11:11:53 +02:00
const fullPath = "./src/assets/schemas/" + path + ".json"
writeFileSync ( fullPath , JSON . stringify ( paths , null , " " ) )
console . log ( "Written meta to " + fullPath )
2023-06-23 17:28:44 +02:00
return Utils . NoNull ( paths . map ( ( p ) = > validateMeta ( p ) ) )
2022-02-09 22:34:02 +01:00
}
2021-11-07 17:52:05 +01:00
function main() {
const allSchemas = ScriptUtils . readDirRecSync ( "./Docs/Schemas" ) . filter ( ( pth ) = >
pth . endsWith ( "JSC.ts" )
2022-09-08 21:40:48 +02:00
)
2023-06-23 16:14:43 +02:00
const allDefinitions : Record < string , JsonSchema > = { }
2021-11-07 17:52:05 +01:00
for ( const path of allSchemas ) {
const dir = path . substring ( 0 , path . lastIndexOf ( "/" ) )
const name = path . substring ( path . lastIndexOf ( "/" ) , path . length - "JSC.ts" . length )
2023-01-15 23:28:02 +01:00
let content = readFileSync ( path , { encoding : "utf8" } )
2021-11-07 17:52:05 +01:00
content = content . substring ( "export default " . length )
let parsed = JSON . parse ( content )
parsed [ "additionalProperties" ] = false
for ( const key in parsed . definitions ) {
const def = parsed . definitions [ key ]
if ( def . type === "object" ) {
def [ "additionalProperties" ] = false
}
}
2023-06-23 16:14:43 +02:00
allDefinitions [ name . substring ( 1 ) ] = parsed
2023-01-29 17:45:48 +01:00
writeFileSync ( dir + "/" + name + ".schema.json" , JSON . stringify ( parsed , null , " " ) , {
encoding : "utf8" ,
} )
2021-11-07 17:52:05 +01:00
}
2023-08-23 11:11:53 +02:00
const errs : string [ ] = [ ]
2023-09-15 01:16:33 +02:00
errs . push ( . . . extractMeta ( "LayerConfigJson" , "layerconfigmeta" , allDefinitions ) )
2023-10-06 23:56:50 +02:00
errs . push ( . . . extractMeta ( "LayoutConfigJson" , "layoutconfigmeta" , allDefinitions ) )
2023-09-15 01:16:33 +02:00
errs . push ( . . . extractMeta ( "TagRenderingConfigJson" , "tagrenderingconfigmeta" , allDefinitions ) )
2023-08-23 11:11:53 +02:00
errs . push (
. . . extractMeta (
"QuestionableTagRenderingConfigJson" ,
"questionabletagrenderingconfigmeta" ,
allDefinitions
)
2023-06-23 16:14:43 +02:00
)
2023-06-23 17:28:44 +02:00
if ( errs . length > 0 ) {
for ( const err of errs ) {
console . error ( err )
}
console . log ( ( errs . length < 25 ? "Only " : "" ) + errs . length + " errors to solve" )
}
2021-11-07 17:52:05 +01:00
}
main ( )