Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Fields.pudu

Pudu132 lines5.8 KB

GitHub ↗
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)] }1112/// A configuration failure naming the dotted location of the value.13export 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}1718/// Every key of an object is one of the allowed names.19export 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}2526/// A reader for the object stored under a key; a missing key reads as empty.27export 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}3637/// Text under a key, or the fallback when it is missing.38export 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}4748/// A flag under a key, or the fallback when it is missing.49export 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}5758/// A list of text under a key; a single text counts as a list of one.59export 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}7778/// Readers for each object of a list under a key; a single object counts as a list of one.79export 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}100101/// A relative directory under a key: empty for the configuration folder, never leaving it.102export 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}110111/// Longest run of leading `../` segments a source folder may have.112const MAX_ASCENT: Int = 8113114/// A relative folder under a key that may start above the configuration folder with `../`115/// segments; the rest must be a portable relative path.116export 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