
Publish.pudu
Pudu153 lines5.5 KB
1/** @Docgen.Docset.Publish — confined, incremental writing of a validated plan */2module PuduLangDocgen.Docset.Publish34import Std.Bytes as Bytes5import Std.Crypto as Crypto6import Std.Fs as Fs7import Std.Io as Io8import Std.Json as Json9import Std.Map as Map10import Std.Path as Path11import PuduLangDocgen.Constants.Codes as Codes12import PuduLangDocgen as Docgen13import PuduLangDocgen.Meta as Meta1415161718export fn publish(root: Str, plan: &Docgen.Plan, state: Str, force: Bool) -> Result[Docgen.Report, Array[Docgen.Diagnostic]] {19 let folder = safe(root) ?20 let previous = recorded(state)21 var hashes: Array[(Str, Docgen.Meta)] = []22 var written: Array[Str] = []23 var unchanged: Array[Str] = []24 var planned: Array[Str] = []25 for artifact in plan.artifacts {26 let hash = Crypto.sha256Hex(artifact.content)27 let target = safe(Path.join(folder, artifact.path)) ?28 planned = planned.push(artifact.path)29 hashes = hashes.push((artifact.path, Docgen.Text(hash)))30 if !force && Map.getOr(&previous, artifact.path, "") == hash && Io.exists(target) {31 unchanged = unchanged.push(artifact.path)32 continue33 }34 place(target, artifact.content) ?35 written = written.push(artifact.path)36 }37 for resource in plan.resources {38 let hash = Bytes.encodeHex(&Crypto.sha512(&resource.content))39 let target = safe(Path.join(folder, resource.path)) ?40 planned = planned.push(resource.path)41 hashes = hashes.push((resource.path, Docgen.Text(hash)))42 if !force && Map.getOr(&previous, resource.path, "") == hash && Io.exists(target) {43 unchanged = unchanged.push(resource.path)44 continue45 }46 placeBytes(target, &resource.content) ?47 written = written.push(resource.path)48 }49 var removed: Array[Str] = []50 for path in Map.keys(&previous) {51 if planned.contains(path) { continue }52 let target = match safe(Path.join(folder, path)) {53 case Ok(found) => found54 case Err(_) => { continue }55 }56 if Io.exists(target) {57 match Io.remove(target) {58 case Ok(_) => { removed = removed.push(path) }59 case Err(_) => {}60 }61 }62 }63 let record = Json.encodePretty(&Json.object(&[("files", Meta.toJson(&Docgen.Fields(hashes)))]))64 place(state, record) ?65 Ok(Docgen.Report{written: written, unchanged: unchanged, removed: removed, diagnostics: plan.diagnostics})66}67686970export fn place(path: Str, text: Str) -> Result[(), Array[Docgen.Diagnostic]] {71 ready(path) ?72 let staging = path + ".docgen-partial"73 match Io.write(staging, text) {74 case Ok(_) => {}75 case Err(reason) => { return Err([Docgen.error(Codes.PUBLISH_FAILED, path, 1, reason)]) }76 }77 settle(staging, path)78}798081export fn placeBytes(path: Str, content: &Bytes) -> Result[(), Array[Docgen.Diagnostic]] {82 ready(path) ?83 let staging = path + ".docgen-partial"84 match Io.writeAllBytes(staging, content) {85 case Ok(_) => {}86 case Err(reason) => { return Err([Docgen.error(Codes.PUBLISH_FAILED, path, 1, show(reason))]) }87 }88 settle(staging, path)89}909192fn settle(staging: Str, path: Str) -> Result[(), Array[Docgen.Diagnostic]] {93 match Fs.rename(staging, path) {94 case Ok(_) => Ok(())95 case Err(reason) => {96 let _cleaned = Io.remove(staging)97 Err([Docgen.error(Codes.PUBLISH_FAILED, path, 1, show(reason))])98 }99 }100}101102103fn recorded(state: Str) -> Map[Str, Str] {104 let text = match Io.read(state) {105 case Ok(found) => found106 case Err(_) => { return mapOf([]) }107 }108 match Json.decode(text) {109 case Ok(found) => {110 var pairs: Array[(Str, Str)] = []111 match Meta.fromJson(&found) {112 case Docgen.Fields(fields) => {113 for (path, value) in Meta.fieldsOf(&fields, "files") {114 match value {115 case Docgen.Text(hash) => { pairs = pairs.push((path, hash)) }116 case _ => {}117 }118 }119 }120 case _ => {}121 }122 mapOf(pairs)123 }124 case Err(_) => mapOf([])125 }126}127128129fn safe(path: Str) -> Result[Str, Array[Docgen.Diagnostic]] {130 let normalized = Path.normalize(path)131 let chain = Path.ancestors(normalized).reverse().push(normalized)132 for part in chain {133 if Io.exists(part) {134 match Fs.metadata(part) {135 case Ok(details) => {136 if details.isSymbolicLink { return Err([Docgen.error(Codes.PUBLISH_FAILED, part, 1, "symbolic links are not followed when publishing")]) }137 if part != normalized && !details.isDirectory { return Err([Docgen.error(Codes.PUBLISH_FAILED, part, 1, "a file stands where a folder is needed")]) }138 }139 case Err(reason) => { return Err([Docgen.error(Codes.PUBLISH_FAILED, part, 1, show(reason))]) }140 }141 }142 }143 Ok(normalized)144}145146147fn ready(path: Str) -> Result[(), Array[Docgen.Diagnostic]] {148 match Io.makeDirectory(Path.directoryOf(path)) {149 case Ok(_) => Ok(())150 case Err(reason) => Err([Docgen.error(Codes.PUBLISH_FAILED, path, 1, reason)])151 }152}153