
Fields.pudu
Pudu132 lines5.8 KB
1/** @Docgen.Configuration.Fields — typed reads of configuration values with exact failures */2module PuduLangDocgen.Configuration.Fields34import PuduLangDocgen.Constants.Codes as Codes5import PuduLangDocgen as Docgen6import PuduLangDocgen.Meta as Meta7import PuduLangDocgen.Paths as Paths89/** @Docgen.Configuration.Reader — the configuration file and the object being read */10export type Reader = { file: Str, at: Str, fields: Array[(Str, Docgen.Meta)] }111213export fn failure(reader: &Reader, key: Str, message: Str) -> Docgen.Diagnostic {14 let place = if reader.at.isEmpty() { key } else if key.isEmpty() { reader.at } else { reader.at + "." + key }15 Docgen.error(Codes.CONFIG_INVALID, reader.file, 1, (if place.isEmpty() { "" } else { place + ": " }) + message)16}171819export fn only(reader: &Reader, allowed: &Array[Str]) -> Result[(), Docgen.Diagnostic] {20 for (key, _value) in reader.fields {21 if !allowed.contains(key) { return Err(failure(reader, key, "unknown option")) }22 }23 Ok(())24}252627export fn nested(reader: &Reader, key: Str) -> Result[Reader, Docgen.Diagnostic] {28 let place = if reader.at.isEmpty() { key } else { reader.at + "." + key }29 match Meta.get(&reader.fields, key) {30 case Some(Docgen.Fields(found)) => Ok(Reader{file: reader.file, at: place, fields: found})31 case Some(Docgen.Nothing) => Ok(Reader{file: reader.file, at: place, fields: []})32 case None => Ok(Reader{file: reader.file, at: place, fields: []})33 case _ => Err(failure(reader, key, "must be an object"))34 }35}363738export fn text(reader: &Reader, key: Str, fallback: Str) -> Result[Str, Docgen.Diagnostic] {39 match Meta.get(&reader.fields, key) {40 case Some(Docgen.Text(found)) => Ok(found)41 case Some(Docgen.Whole(count)) => Ok(show(count))42 case Some(Docgen.Nothing) => Ok(fallback)43 case None => Ok(fallback)44 case _ => Err(failure(reader, key, "must be text"))45 }46}474849export fn flag(reader: &Reader, key: Str, fallback: Bool) -> Result[Bool, Docgen.Diagnostic] {50 match Meta.get(&reader.fields, key) {51 case Some(Docgen.Flag(found)) => Ok(found)52 case Some(Docgen.Nothing) => Ok(fallback)53 case None => Ok(fallback)54 case _ => Err(failure(reader, key, "must be true or false"))55 }56}575859export fn texts(reader: &Reader, key: Str) -> Result[Array[Str], Docgen.Diagnostic] {60 match Meta.get(&reader.fields, key) {61 case Some(Docgen.Text(found)) => Ok([found])62 case Some(Docgen.Items(listed)) => {63 var result: Array[Str] = []64 for held in listed {65 match held {66 case Docgen.Text(found) => { result = result.push(found) }67 case _ => { return Err(failure(reader, key, "must list text values")) }68 }69 }70 Ok(result)71 }72 case Some(Docgen.Nothing) => Ok([])73 case None => Ok([])74 case _ => Err(failure(reader, key, "must be text or a list of text"))75 }76}777879export fn objects(reader: &Reader, key: Str) -> Result[Array[Reader], Docgen.Diagnostic] {80 let place = if reader.at.isEmpty() { key } else { reader.at + "." + key }81 match Meta.get(&reader.fields, key) {82 case Some(Docgen.Fields(found)) => Ok([Reader{file: reader.file, at: place, fields: found}])83 case Some(Docgen.Items(listed)) => {84 var result: Array[Reader] = []85 var index = 086 for held in listed {87 match held {88 case Docgen.Fields(found) => { result = result.push(Reader{file: reader.file, at: place + "[" + show(index) + "]", fields: found}) }89 case _ => { return Err(failure(reader, key, "must list objects")) }90 }91 index = index + 192 }93 Ok(result)94 }95 case Some(Docgen.Nothing) => Ok([])96 case None => Ok([])97 case _ => Err(failure(reader, key, "must be an object or a list of objects"))98 }99}100101102export fn directory(reader: &Reader, key: Str, fallback: Str) -> Result[Str, Docgen.Diagnostic] {103 let written = text(reader, key, fallback) ?104 let trimmed = if written.endsWith("/") { written.take(written.length() - 1) } else { written }105 if trimmed.isEmpty() || trimmed == "." { return Ok("") }106 let cleaned = if trimmed.startsWith("./") { trimmed.drop(2) } else { trimmed }107 if !Paths.relative(cleaned) { return Err(failure(reader, key, "must be a relative folder inside the project: " + written)) }108 Ok(cleaned)109}110111112const MAX_ASCENT: Int = 8113114115116export fn sourceFolder(reader: &Reader, key: Str, fallback: Str) -> Result[Str, Docgen.Diagnostic] {117 let written = text(reader, key, fallback) ?118 var rest = if written.endsWith("/") { written.take(written.length() - 1) } else { written }119 if rest.startsWith("./") { rest = rest.drop(2) }120 var ascent = 0121 while rest == ".." || rest.startsWith("../") {122 ascent = ascent + 1123 rest = if rest == ".." { "" } else { rest.drop(3) }124 }125 if ascent > MAX_ASCENT { return Err(failure(reader, key, "leads more than " + show(MAX_ASCENT) + " folders above the project: " + written)) }126 if !rest.isEmpty() && rest != "." && !Paths.relative(rest) { return Err(failure(reader, key, "must be a relative folder: " + written)) }127 var parts: Array[Str] = []128 for _step in 0..ascent { parts = parts.push("..") }129 if !rest.isEmpty() && rest != "." { parts = parts.push(rest) }130 Ok(parts.join("/"))131}132