nickel icon indicating copy to clipboard operation
nickel copied to clipboard

Autogenerated contracts with configurable configurations

Open vkleen opened this issue 2 years ago • 2 comments

Is your feature request related to a problem? Please describe. When trying to use the partial configurations approach in Nickel, it is common to write a top-level record like

{
  inputs | not_exported = { ... },
  output1 = ...,
  output2 = ...,
}

In this case, the configuration is targeting an external tool that expects only the fields output1 and output2. But to make the Nickel configuration configurable, we decided to define these fields with the help of values in inputs. The field inputs is marked as not_exported so that the external tool doesn't complain.

Now I would like to generate a contract describing the configuration format that the external tool accepts. When such a contract is automatically generated, say by tf-ncl or json-schema-to-nickel, it of course won't know about the inputs field and validation will fail.

Describe the solution you'd like I'm looking for an idiomatic solution to using automatically generated contracts with extra not_exported fields. Such a solution should ideally support the "configurable configuration" paradigm enabled by Nickel and be introspectable with nickel query.

Describe alternatives you've considered

  1. We could introduce a standard library function std.export, or similar, that evaluates a record while recursively removing not_exported fields, just like the serialization process would. Then the autogenerated contract could call this function before starting the validation. The downside here would be that the contract would become harder to understand for the LSP; it would need to be able to ignore this extra pre-processing step.

  2. Add a special way of applying a contract check to serialized output. Maybe with a syntax like

    { ... } | (serialized_as Contract)  
    

    This is a more specialized version of the std.export idea. As such it might be easier to update the LSP and implement in a lazy way, pushing serialize_as contracts into relevant fields.

  3. Do nothing. In Nickel we can leverage functions to do arbitrary post-processing, so validation checks ignoring certain fields can of course be implemented by hand. The downside would be more complicated contract generation, less transparency for the LSP and probably some code duplication.

vkleen avatar Jul 12 '23 10:07 vkleen

Might be very naive, but how about reverting the paradigm and having parameters living at the root level, and the output living inside a config or an output field, like NixOS modules? I guess both dual approaches are valid, but you make a point that the "output at the root, input inside a field" doesn't play very well with existing contracts (generated or not). I believe this is how tf-ncl works in fact, or used to work, at least?

{
  input1 | Contract1,
  input2 | Contract2,
 ...
  output | GeneratedContract = {

  }
}

In any case, there something to be fleshed out about this idea of where do inputs and outputs live, and how do we compose many configurations together and make it work, in particular if you need some kind of "namespacing" (differentiating the foo coming from module1 and the foo coming from module2)

yannham avatar Jul 12 '23 13:07 yannham

That is indeed how tf-ncl works. The namespacing is an interesting concerns that I actually hadn't thought about yet. The other issue with the output field approach is that when exporting the configuration you will always need to specify that it's just the output field that should be exported. E.g. run nickel export <<<'(import "foo.ncl").output' and similar contortions.

In the GitHub workflow example that I had in mind when writing up this issue, the whole thing worked very nicely with not_exported annotations for the input fields. Until I tried to validate the overall output, that is.

vkleen avatar Jul 12 '23 13:07 vkleen