// Multi-file specs. goctl lets a `.api` pull in others — `import "user.api"`, or a
// grouped `import ( … )` — and every path is relative to the file that wrote it, so
// resolving one takes both. Reading files is not this package's job (it stays pure
// and all-backend), so the split is: `deps` tells a caller which files a source
// needs, and `parse_all` merges the sources it collected into one `Spec` — imports
// first, in the order they were declared.
///|
/// Whether `c` separates path segments. A spec path can arrive from a Windows
/// shell, so both spellings count.
fn is_sep(c : UInt16) -> Bool {
c == '/' || c == '\\'
}
///|
/// The directory `p` lives in, without its trailing separator; "" when `p` names a
/// file in the current directory.
fn dir_of(p : String) -> String {
let mut cut = -1
for i = 0; i < p.length(); i = i + 1 {
if is_sep(p[i]) {
cut = i
}
}
if cut < 0 {
""
} else {
p[0:cut].to_owned()
}
}
///|
/// Whether `p` names a location by itself rather than relative to something.
fn is_absolute(p : String) -> Bool {
if p.length() == 0 {
return false
}
if is_sep(p[0]) {
return true
}
p.length() >= 2 && p[1] == ':'
}
///|
/// `p` with its `.` and `..` segments folded away and `/` as the separator. A `..`
/// that climbs past the start is kept, since dropping it would quietly change which
/// file is meant.
fn normalize(p : String) -> String {
let out : Array[String] = []
let mut seg = ""
let lead = if p.length() > 0 && is_sep(p[0]) { "/" } else { "" }
for i = 0; i <= p.length(); i = i + 1 {
if i == p.length() || is_sep(p[i]) {
if seg == ".." && out.length() > 0 && out[out.length() - 1] != ".." {
out.pop() |> ignore
} else if seg != "." && seg != "" {
out.push(seg)
}
seg = ""
} else {
seg = seg + p[i:i + 1].to_owned()
}
}
let mut joined = ""
for i = 0; i < out.length(); i = i + 1 {
joined = if i == 0 { out[i] } else { joined + "/" + out[i] }
}
lead + joined
}
///|
/// An `import` path resolved against the file that declared it: taken relative to
/// that file's directory, with `.`/`..` folded away. An already-absolute path is
/// left where it points.
pub fn resolve_import(from : String, path : String) -> String {
if is_absolute(path) {
return normalize(path)
}
let dir = dir_of(from)
normalize(if dir == "" { path } else { dir + "/" + path })
}
///|
/// The files `source` imports, each resolved against `from` — the path `source`
/// itself was read from. A caller that can read files walks a multi-file spec with
/// this: read a file, follow its `deps`, and hand everything it collected to
/// `parse_all`.
pub fn deps(source : String, from? : String = "") -> Array[String] {
let out : Array[String] = []
for raw in parse_lenient(source).imports {
out.push(resolve_import(from, raw))
}
out
}
///|
/// One file of the import graph, scanned.
priv struct Scan {
path : String
spec : Spec
issues : Array[(Int, String)]
}
///|
/// Scan `source` and everything it imports, depth-first and each file once, so an
/// imported file lands in `out` before the file that imported it.
fn walk(
path : String,
source : String,
files : Map[String, String],
seen : Map[String, Bool],
out : Array[Scan],
) -> Unit raise SpecError {
seen[path] = true
let (spec, issues) = read(source)
for raw in spec.imports {
let next = resolve_import(path, raw)
if seen.contains(next) {
continue
}
match files.get(next) {
Some(src) => walk(next, src, files, seen, out)
None => raise Missing(file=path, path=raw)
}
}
out.push({ path, spec, issues, })
}
///|
/// Parse `source` — the spec at `from` — together with everything it imports, taken
/// from `files` (a map from resolved path to source, the shape `deps` resolves to).
/// What the imports describe is merged in first, in the order they were declared,
/// then the importing file's own: types, routes and `@server` groups all end up in
/// one `Spec`. A file is scanned once however many times it is imported, so a cycle
/// terminates.
///
/// The service name is the last one declared, so the importing file's own `service`
/// block wins and a spec that does nothing but import still takes the name from what
/// it imported.
///
/// An `import` naming a file that is not in `files` raises `Missing`; a file that
/// does not parse raises its first complaint the way `parse` does, with the file it
/// came from named alongside the line.
pub fn parse_all(
source : String,
files : Map[String, String],
from? : String = "",
) -> Spec raise SpecError {
let units : Array[Scan] = []
walk(from, source, files, Map([]), units)
for u in units {
if u.issues.length() > 0 {
let mut first = u.issues[0]
for it in u.issues {
if it.0 < first.0 {
first = it
}
}
let msg = if u.path == from { first.1 } else { u.path + ": " + first.1 }
raise Syntax(line=first.0, msg~)
}
}
let mut service = "app"
let routes : Array[Route] = []
let types : Array[TypeDef] = []
let info : Array[(String, String)] = []
let groups : Array[Group] = []
let imports : Array[String] = []
for u in units {
if u.spec.service != "app" {
service = u.spec.service
}
for r in u.spec.routes {
routes.push(r)
}
for t in u.spec.types {
types.push(t)
}
for kv in u.spec.info {
info.push(kv)
}
for g in u.spec.groups {
groups.push(g)
}
if u.path != from {
imports.push(u.path)
}
}
{ service, routes, types, info, groups, imports, }
}