diff --git a/.gitignore b/.gitignore index a50b1b3..618d480 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules .tests .vscode +.repl diff --git a/fixture-file b/fixture-file new file mode 100644 index 0000000..6246c5a --- /dev/null +++ b/fixture-file @@ -0,0 +1 @@ +this is a fixture! diff --git a/src/FS.mad b/src/FS.mad new file mode 100644 index 0000000..5675913 --- /dev/null +++ b/src/FS.mad @@ -0,0 +1,21 @@ +import Dict from "Dictionary" +import { fromMaybe } from "Maybe" +import Process from "Process" +import String from "String" + + + +normalize :: String -> String +export normalize = (input) => pipe( + Dict.get("HOME"), + map(String.replace("~", $, input)), + fromMaybe(input), +)(Process.Env) + +// TODO: check behavior for non-OSX files +tempdir :: String -> String +export tempdir = (file) => pipe( + Dict.get("TMPDIR"), + fromMaybe("/private/var/tmp/"), + mappend(file), +)(Process.Env) diff --git a/src/Jam.mad b/src/Jam.mad index 49ef64f..2be2746 100644 --- a/src/Jam.mad +++ b/src/Jam.mad @@ -1,87 +1,185 @@ import type { Error } from "IO" import File from "File" -import { always } from "Function" +import { always, complement, ifElse, when } from "Function" +import {} from "Monad" import String from "String" import Wish from "Wish" +import FS from "@/FS" -// Preserve(dirty, file) -export type Preserve = Preserve(Boolean, String) -export alias Memory a = { get :: {} -> a, isDirty :: {} -> Boolean, set :: a -> a } -// accessors +// Preserve +// - clean - has the value been written to source? +// - sourceFile - where does the value get saved? +// - value - the preserved value +export type Preserve = Preserve(Boolean, String, String) +export alias Memory = Wish Error Preserve -isDirty :: Preserve -> Boolean -export isDirty = where { - Preserve(d, _) => + +//## ACCESSORS + +/** + * Has the value been saved? + * @since 0.0.1 + */ +isClean :: Preserve -> Boolean +export isClean = where { + Preserve(d, _, _) => d } +isDirty :: Preserve -> Boolean +export isDirty = complement(isClean) + +/** + * Where is the value saved? + * @since 0.0.1 + */ source :: Preserve -> String export source = where { - Preserve(_, f) => + Preserve(_, f, _) => f } -// constructors +/** + * What is the saved value? + * @since 0.0.1 + */ +value :: Preserve -> String +export value = where { + Preserve(_, _, v) => + v +} + +//## CONSTRUCTORS -export create = Preserve(false) +/** + * Create a Preserved value in memory + * @since 0.0.1 + */ +export create = (file, v) => pipe( + FS.normalize, + Preserve(true, $, v), +)(file) -// methods +/** + * Load a Preserved value from source + * @since 0.0.1 + */ +load :: String -> Memory +export load = (file) => pipe( + FS.normalize, + File.read, + map(Preserve(true, file)), +)(file) -save :: Preserve -> String -> Wish Error {} -export save = (src, data) => where(src) { - Preserve(dirty, file) => - dirty ? File.write(file, data) : Wish.good({}) +//## METHODS + +/** + * Set the preserved value and set clean to false + */ +set :: String -> Preserve -> Preserve +export set = (x, p) => where(p) { + Preserve(d, s, _) => + Preserve(false, s, x) } -forceSave :: Preserve -> String -> Wish Error {} -export forceSave = (src, data) => where(src) { - Preserve(dirty, file) => - pipe( - File.write(file), - map(always({})), - )(data) +/** + * Transform the preserved value and set clean to false + * @since 0.0.1 + */ +update :: (String -> String) -> Preserve -> Preserve +export update = (fn, p) => where(p) { + Preserve(d, s, v) => + Preserve(false, s, fn(v)) } -load :: Preserve -> Wish Error String -export load = where { - Preserve(_, file) => - File.read(file) +/** + * Set dirtiness + * @since 0.0.1 + */ +setClean :: Preserve -> Preserve +export setClean = where { + Preserve(_, s, v) => + Preserve(true, s, v) } -flush :: Preserve -> String -> Wish Error {} -export flush = (src, data) => where(src) { - Preserve(dirty, file) => - if (dirty) { +/** + * Set cleanliness + * @since 0.0.1 + */ +setDirty :: Preserve -> Preserve +export setDirty = where { + Preserve(_, s, v) => + Preserve(false, s, v) +} + +isEmpty :: Preserve -> Boolean +export isEmpty = where { + Preserve(_, _, v) => + v == "" +} + +/** + * Save the file to source only if it is dirty + * @since 0.0.1 + */ +save :: Preserve -> Memory +export save = (pres) => ifElse( + isDirty, + pipe( + setClean, + flush, + ), + Wish.good, +)(pres) + +/** + * Mount the preserved value from source (or write a default value if no value is present) + * @since 0.0.1 + */ +ensure :: String -> String -> Memory +export ensure = (src, defaultValue) => pipe( + load, + chain( + (loaded) => if (isEmpty(loaded)) { pipe( - File.write(file), - map(always({})), - )(data) + set(defaultValue), + flush, + )(loaded) } else { - Wish.good({}) - } + Wish.good(loaded) + }, + ), +)(src) + +/** + * Write the value to source and set clean to true + * @since 0.0.1 + */ +flush :: Preserve -> Memory +export flush = (pres) => { + v = value(pres) + f = source(pres) + return pipe( + File.write(f), + map(always(Preserve(true, f, v))), + )(v) } -remember :: Preserve -> String -> Wish Error (Memory String) -export remember = (src, initialValue) => { - x = initialValue - changed = false - controller = { - get: () => x, - set: (y) => if (!changed) { - y - } else do { - x := y - changed := true - return y - }, - isDirty: () => changed, - } +/** + * Write the value to a temp file and set clean to true + * @since 0.0.1 + */ +backup :: Preserve -> Memory +export backup = (pres) => { + v = value(pres) + raw = source(pres) + tmp = FS.tempdir(raw) return pipe( - forceSave(src), - map(always(controller)), - )(x) + File.write(tmp), + map(always(Preserve(true, raw, v))), + )(v) } diff --git a/src/Jam.spec.mad b/src/Jam.spec.mad new file mode 100644 index 0000000..37f955f --- /dev/null +++ b/src/Jam.spec.mad @@ -0,0 +1,92 @@ +import { Error, assertEquals, test } from "Test" +import Wish from "Wish" + +import FS from "@/FS" +import { + Preserve, + create, + ensure, + isClean, + isDirty, + isEmpty, + load, + set, + setClean, + setDirty, + source, + update, + value, +} from "@/Jam" +import { asyncReport, report } from "@/test" + + + +CLEAN = Preserve(true, "src", "raw") +DIRTY = Preserve(false, "src", "raw") + +report(isClean, "isClean", [#[CLEAN, true], #[DIRTY, false]]) +report(isDirty, "isDirty", [#[CLEAN, false], #[DIRTY, true]]) +report(source, "source", [#[DIRTY, "src"]]) +report(value, "value", [#[DIRTY, "raw"]]) + +report( + create("~/whatever"), + "create", + [#["blah", Preserve(true, FS.normalize("~/whatever"), "blah")]], +) + +report( + set("test value!"), + "set", + [#[Preserve(true, "./test-location", "yyy"), Preserve(false, "./test-location", "test value!")]], +) + +report( + update((raw) => "hello " ++ raw), + "update", + [ + #[ + Preserve(true, "./test-location", "there"), + Preserve(false, "./test-location", "hello there"), + ], + ], +) + +report( + setClean, + "setClean", + [#[Preserve(false, "./clean-me", "value"), Preserve(true, "./clean-me", "value")]], +) + +report( + setDirty, + "setDirty", + [#[Preserve(true, "./me-dirty", "value"), Preserve(false, "./me-dirty", "value")]], +) + +report( + isEmpty, + "isEmpty", + [ + #[Preserve(true, "./some-place", ""), true], + #[Preserve(true, "./some-place", "not empty"), false], + ], +) + +asyncReport( + pipe( + load, + Wish.mapRej((_) => Error("Broken!")), + ), + "load", + [#["./fixture-file", Preserve(true, "./fixture-file", "this is a fixture!\n")]], +) + +asyncReport( + pipe( + ensure("./fixture-file"), + Wish.mapRej((_) => Error("Broken!")), + ), + "ensure", + [#["default value", Preserve(true, "./fixture-file", "this is a fixture!\n")]], +) diff --git a/src/Main.mad b/src/Main.mad index 3781804..b38a8b3 100644 --- a/src/Main.mad +++ b/src/Main.mad @@ -7,21 +7,34 @@ import Jam from "@/Jam" main = () => { - jam = Jam.create("~/.jamfile-example") - IO.pTrace("JAM!", jam) - + src = "~/.jamfile-example" + // LOAD A FILE + pipe( + Jam.load, + Wish.fulfill( + pipe( + IO.pTrace("LOAD BAD"), + noop, + ), + pipe( + IO.pTrace("LOAD GOOD"), + noop, + ), + ), + )(src) + // LOAD A FILE BUT ENSURE THERE'S AN INITIAL VALUE pipe( - Jam.remember($, "save data!"), - IO.pTrace("remembering..."), + Jam.ensure($, "initial value"), + IO.pTrace("loaded?"), Wish.fulfill( pipe( - IO.pTrace("BAD"), + IO.pTrace("ENSURE BAD"), noop, ), pipe( - IO.pTrace("GOOD"), + IO.pTrace("ENSURE GOOD"), noop, ), ), - )(jam) + )(src) } diff --git a/src/Test.mad b/src/Test.mad new file mode 100644 index 0000000..78366c6 --- /dev/null +++ b/src/Test.mad @@ -0,0 +1,43 @@ +import { mapWithKey, values } from "Dictionary" +import Term from "Terminal" +import { assertEquals, test } from "Test" + + + +printReport = (name, i) => `${name}\n ↳ ${ + pipe( + show, + Term.ansiColor([Term.ansi.FormatBold, Term.ansi.FGBrightYellow]), + )(i) +}` + +createTest = (name, fn, i, o) => test(printReport(name, i), () => assertEquals(fn(i), o)) + +export report = (fn, name, listInOut) => pipe( + map(where { #[i, o] => createTest(name, fn, i, o) }), +)(listInOut) + +templateChainTest = (name, fn, i, o) => test( + printReport(name, i), + () => pipe( + fn, + chain(assertEquals(o)), + )(i), +) + + +export asyncReport = (fn, name, listInOut) => pipe( + map(where { #[i, o] => templateChainTest(name, fn, i, o) }), +)(listInOut) + + +export reportBook = (fn, name, testDict) => pipe( + mapWithKey(createTest(name, fn)), + values, +)(testDict) + +caseN2 :: (a -> b -> c) -> #[a, b] -> c +export caseN2 = (fn, pair) => where(pair) { + #[a, b] => + fn(a, b) +}