moonknob

    Which of two settings wins, and what to do when they disagree: the precedence every moonbitstack library shares. Not a configuration loader; that is moonconf.

    moonbit
    configuration
    config
    precedence
    override
    merge
    conflict
    options
    settings
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    15 hours ago
    Downloads
    55

    #moonknob

    Which of two settings wins, and what to do when they disagree. Every moonbitstack library that takes a configuration record and flat arguments beside it resolves the two through this module, so the rules below hold across the family.

    #Configuration precedence

    library preset < the record passed in < this call's argument of the same name

    The more specific a setting, the later it applies: the order Axios, Terraform and Spring Boot share. Two parameters change it, and both take a default chosen per function, written in that function's documentation:

    • wins : Wins is Extra (the argument wins) or Base (the record wins). Base is for a function whose starting point is an authority, such as the header a JWT signature is made under.
    • clash : OnClash[T] says what happens when a setting is given twice with different values:

    VariantResultSays
    Ignorewhat wins chosenothing
    Warn(f)what wins choseone line to f
    Panicabortsthe message
    Handle(f)what f returnsf gets a Clash[T]: the names, the base, the merged value

    The same value given twice is not a clash, and Warn and Handle are not called for it. The library never prints: Warn takes a function, so the line goes to moonlog or wherever the program writes.

    #Writing a function that takes a record

    struct Options {
    timeout : Int32
    retries : Int32
    }

    let options : Options = { timeout: 30, retries: 3 }

    fn connect(
    options~ : Options = options,
    timeout? : Int32,
    retries? : Int32,
    wins~ : @moonknob.Wins = Extra,
    clash~ : @moonknob.OnClash[Options] = Ignore,
    ) -> Options {
    let merge = @moonknob.Merge::new(wins)
    let merged = {
    timeout: merge.pick("timeout", options.timeout, timeout),
    retries: merge.pick("retries", options.retries, retries),
    }
    merge.settle(clash, options, merged, "connect: options and arguments")
    }

    pick takes one field: the name, the record's value, and the argument as the caller gave it (None when it did not). union does the same for two maps, key by key, with the keys of the first in their order and then the new ones of the second. settle applies clash to what was gathered. A function that gathers names some other way calls OnClash::settle with them.

    #Calling one

    let mine = { timeout: 5, retries: 1 }
    let a = connect(options=mine, timeout=10)
    let b = connect(options=mine, timeout=10, wins=Base)
    let c = connect(options=mine, timeout=10, clash=Warn(line => println(line)))
    let d = connect(
    options=mine,
    timeout=10,
    retries=9,
    clash=Handle(seen => { ..seen.merged, retries: seen.base.retries }),
    )

    a and c time out after 10; b keeps the record's 5, and d keeps the record's retries while taking the argument's timeout. c printed connect: options and arguments: timeout given twice with different values.

    Run moon run src/examples/tour for the same in one go.

    #Licence

    Apache-2.0.

    Clash

    pub(all) struct Clash[T] {
    names : Array[String]
    base : T
    merged : T
    } derive(Eq,
    Debug
    )

    Settings given twice with different values, as Handle receives them.

    Clash::equal

    fn[T : Eq] Clash::equal(Clash[T], Clash[T]) -> Bool

    Clash::not_equal

    fn[T : Eq] Clash::not_equal(x : Clash[T], y : Clash[T]) -> Bool

    Merge

    type Merge

    Lays what a caller gave over what a function starts from, one setting at a time, and keeps the names of those that disagreed for settle.

    let merge = @moonknob.Merge::new(wins) let merged = { ..policy, leeway: merge.pick("leeway", policy.leeway, leeway), expiry: merge.pick("expiry", policy.expiry, expiry), } merge.settle(clash, policy, merged, "jwt.verify: policy and arguments")

    Merge::names

    fn Merge::names(self : Merge) -> ArrayView[String]

    The names of the settings that disagreed so far.

    Merge::new

    fn Merge::new(wins : Wins) -> Merge

    Merge::pick

    fn[V : Eq] Merge::pick(self : Merge, name : String, base : V, given : V?) -> V

    One setting. given is None when the caller did not give it, and then base stands; a value equal to base stands without being a clash.

    Merge::settle

    fn[T] Merge::settle(self : Merge, clash : OnClash[T], base : T, merged : T, site : String) -> T

    clash applied to what was gathered: see [OnClash::settle].

    Merge::union

    fn[V : Eq] Merge::union(self : Merge, base : Map[String, V], extra : Map[String, V]) -> Map[String, V]

    Two maps as one: every key of both, in the order base then extra has them, with wins deciding a key they both hold with different values.

    OnClash

    pub(all) enum OnClash[T] {
    Ignore
    Warn((String) -> Unit)
    Panic
    Handle((Clash[T]) -> T)
    }

    What to do when a setting is given twice with different values.

    The same value given twice is not a clash: there is only one answer, and nothing is reported.

    OnClash::settle

    fn[T] OnClash::settle(self : OnClash[T], base : T, merged : T, names : Array[String], site : String) -> T

    What a function ends up with, given what it started from, the two laid together, and the settings that disagreed. With no names there was no clash, and merged comes back whatever self is.

    site is the call and its two sources, for the warning and the abort: "jwt.verify: policy and arguments".

    Wins

    pub(all) enum Wins {
    Base
    Extra
    } derive(Eq,
    Debug
    )

    Which side wins when a setting is given twice: once in what a function starts from, once in what the caller lays over it.

    A function that takes a configuration record usually takes flat arguments beside it, one per setting a caller changes often, and the more specific of the two is applied last: library preset, then the record, then this call's arguments. That is Extra. Base turns it round for a function whose starting point is an authority rather than a default, such as the header a signature is about to be made under.

    Wins::equal

    fn Wins::equal(Wins, Wins) -> Bool

    Wins::not_equal

    fn Wins::not_equal(x : Wins, y : Wins) -> Bool

    Wins::to_repr