
Rules.pudu
Pudu83 lines4.8 KB
1/** @Docgen.Configuration.Rules — cross-field rules a read configuration must satisfy */2module PuduLangDocgen.Configuration.Rules34import PuduLangDocgen.Constants.Codes as Codes5import PuduLangDocgen as Docgen6import PuduLangDocgen.Paths as Paths7import PuduLangDocgen.References as References8import PuduLangValidator.Result as Validation9import PuduLangValidator.Rule as Rule10import PuduLangValidator.Rules.Collection as Each11import PuduLangValidator.Validator as Validator1213/** @Docgen.Configuration.Settings — the configuration values these rules judge */14export type Settings = {15 output: Str,16 sitemap: Bool,17 baseUrl: Str,18 priority: Str,19 changefreq: Str,20 xref: Array[Str],21 services: Array[Str],22 destinations: Array[Str],23 repository: Str,24 renderer: Array[Str]25}262728export const FREQUENCIES: Array[Str] = ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]293031export fn check(file: Str, settings: &Settings) -> Array[Docgen.Diagnostic] {32 let outcome = Validator.validate(&validator(), *settings)33 outcome.errors.map(|failure: Validation.Failure| Docgen.error(Codes.CONFIG_INVALID, file, 1, failure.propertyName + ": " + failure.errorMessage))34}353637383940fn validator() -> Validator.Validator[Settings] {41 let output = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("build.output", |held: Settings| held.output), |value: Str| !value.isEmpty()), "must name a folder other than the project folder"))42 let baseUrl = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.baseUrl", |held: Settings| held.baseUrl), |value: Str| web(value)), "must be an absolute http or https address"), |held: Settings| held.sitemap))43 let priority = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.priority", |held: Settings| held.priority), |value: Str| value.isEmpty() || fraction(value)), "must be a number from 0.0 to 1.0"), |held: Settings| held.sitemap))44 let frequency = Rule.build(Rule.when(Rule.withMessage(Rule.must(Rule.ruleFor("build.sitemap.changefreq", |held: Settings| held.changefreq), |value: Str| value.isEmpty() || FREQUENCIES.contains(value)), "must be one of " + FREQUENCIES.join(", ")), |held: Settings| held.sitemap))45 let xref = Each.buildEach(Each.withMessage(Each.must(Each.ruleForEach("build.xref", |held: Settings| held.xref), |value: Str| web(value) || Paths.relative(value)), "must be a web address or a relative path inside the project"))46 let services = Each.buildEach(Each.withMessage(Each.must(Each.ruleForEach("build.xrefService", |held: Settings| held.services), |value: Str| web(value) && value.contains(References.UID_SLOT)), "must be a web address holding \{uid\}"))47 let destinations = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("metadata.dest", |held: Settings| held.destinations), |values: Array[Str]| distinct(&values)), "each API source needs its own destination folder"))48 let repository = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("build.gitContribute.repo", |held: Settings| held.repository), |value: Str| value.isEmpty() || web(value)), "must be an http or https address"))49 let renderer = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("pdf.renderer", |held: Settings| held.renderer), |values: Array[Str]| values.isEmpty() || (!values[0].isEmpty() && values.filter(|part: Str| part.contains("\{output\}")).length() > 0)), "must name a program and an argument holding \{output\}"))50 var result = Validator.create()51 for rule in [output, baseUrl, priority, frequency, xref, services, destinations, repository, renderer] { result = Validator.add(result, rule) }52 result53}545556fn web(value: Str) -> Bool {57 let lowered = value.toLower()58 (lowered.startsWith("https://") || lowered.startsWith("http://")) && Paths.href(value)59}606162fn fraction(text: Str) -> Bool {63 let parts = text.split(".")64 if parts.length() > 2 || parts[0].isEmpty() { return false }65 for part in parts {66 for character in part.chars() {67 if character < '0' || character > '9' { return false }68 }69 }70 if parts[0] == "1" { return parts.length() == 1 || parts[1].replace("0", "").isEmpty() }71 parts[0] == "0"72}737475fn distinct(values: &Array[Str]) -> Bool {76 var seen: Array[Str] = []77 for value in *values {78 if seen.contains(value) { return false }79 seen = seen.push(value)80 }81 true82}83