bcl.go

module
v0.0.0-...-8de7dcd Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 22, 2024 License: MIT

README

BCL - Block Config Language

BCL is - another - human centric general configurateion language.

It is designed around the specific case of J5 Schemas, initially to specify the schema itself, but also to be a codec of J5 Schema itself, as a schema driven config language.

Goals:

  • All about the humans reading and writing.
  • Ability to extend the language over time.
  • Support 'Doc' blocks, large (markdown) text bodies attached to elements
  • Familiar for {} style programmers. Small, easy to learn and all those good things we always try to achieve.
Why this and not X?

"Because I wanted to see if I could" is probably the most honest answer, but there were a few things missing in the available languages:

  • UCL is very close to what I want, and with macros, BCL could probably even be implemented in UCL. ... I found UCL while writing these docs and linking to HCL, no regrets, it doesn't have a doc block.

  • HCL would suit well, and has really nice elements, but I don't want to get involved in whatever copyleft and right problem is happening there.

  • JSON Schema is good, and close-ish to what we want for the schema definitions. Swagger/OAS, however, is too based in REST and ends up being an exercise in duck punching.

  • JSON - no comments, no set structure

  • JSON + Schema + extensions for comments: we are still inventing a 'language', but without control over the syntax. This applies for YAML, XML as well

  • YAML - I love yaml, but my eyes are getting dim, I can't figure out how nested I am.

  • TOML - Great for configuration, but it is really only designed for a few levels of nesting at best.

  • PKL - looks interesting, but is typed in the config file itself rather than filling in a pre-defined schema.

There are single purpose langiuages which also compare pretty well:

  • PROTO is cool, single-purpose - that purpose being API definitions, and the basis for J5, but the extensions are getting out of hand. The 'defaults' we want to define on all fields are all extension options, so we would end up writing a validator anyway.

  • Nginx Config format is awesome. Much like HCL, this will look familiar to anyone who has worked with Nginx.

Status

Unstable and immature. (but enough about me...)

Structure

BCL works in layers. I mean, all languages do but the API for this is available at each layer.

Layer 1: Base Syntax

Defines the Assign and Block structures from a string. Has no specific structure or file format. The 'lexer' and 'parser' parts of a language.

The API exposes tools to BYO schema and parse it directly, defining handlers for Assignments, Blocks and Docs.

BYO Schema, the API allows you to build tools to walk and valudate schemas, including line errors for both syntax and schema issues.

Layer 2: Schema

Defining the types of blocks as J5 Schemas.

Deifned in .proto for now becuase this doesn't work yey, but passed in to the library as compiled so when it does work they will also be defined in BCL

Layer 3: Modules

Similar to Go and Buf-Proto, the directory of a file specifies a 'package'.

Like Go, but not proto, the file name does not matter, you can freely move content between files without changing the result. Imports import the package, not the file, and there is no 'index' file like js modules.

The build in directives export, import, partial and include allow merging and reference between elements as a nested structure.

Layer 1, Syntax

// Assignment
foo = "bar"

// Directive
foo bar "baz"

// Doc
| documentation
| multiline
| string

/*
  multi line comment
*/


// Block
foo bar baz {
  | documentation

  key = value
}

Base elements

An ident starts with a unicode letter character, upper and lower, followed by letters or numbers. [a-zA-Z][a-zA-Z0-9]*

A reference is a series of ident separated by periods. ident(.ident)*

A literal is a string, number, or boolean.

  • Strings, quoted with ""
  • Numbers, integers or floats specified 1.1 or 1
  • Booleans, true or false with no quotes
  • null, for un-setting things like partial overrides

Context defines the type of the literal, so 1 and 1.1 are both valid for floats (i.e. you don't have to write 1.0)

Strings may span multiple lines, by escaping the end of line, and may also escape quotes.

key = "This is a string"
key = "This \
is a string"
key = "This is a "string""
Comment

Comments are C-style, // for single line, /* */ for multi-line.

Assignment
key = value

Keys are 'reference' type. Values are 'literal' type.

Directive
keyword value
package foo.bar
field foo string

The available keywords for directives are context dependent, and built in. The context and keyword defines the number and type of the arguments.

Keys are 'reference' type. Values are 'literal' type.

Block

The root of the document is a body, which is a series of Assignments, Directives, comments and Definitions.

Elements use curly braces {} to define the body.

field foo string{
  // ... body elements
}

Skipping the body is equivalent to an empty body, if there is nothing to define the body you can just skip it.

field foo string
field foo string {
}
Doc

Docs are like multi-line comments, but specifically used to describe elements for documentation in generated code and schemas, rather than comment about the code.

Docs are valid in all Block Bodies (including the root), but specific schemas may restrict.

Docs can be specified in two ways:

field foo string | Inline Doc on a single line
field foo string {
  | Doc in the body, which can span multiple lines
  | but must be specified at the start of the block.
}

Inline descriptions don't work with body blocks, i.e. the following is not valid

field foo string { | Inline
    // ... body elements
}

In Module mode, Docs are not valid at the file level, we don't want to end up with the doc.go thing, that's what README.md is for.

The intention (and syntax hilighting) is that descriptions are markdown documents, however in Layer 1 these are just big strings.

In module mode, start carefully until we can build some validation rules:

  • Using bold, italic and code: Good to go.
  • Headings: Avoid until we figure out what the nesting would be. It certainly wouldn't make sense to have a full document with headings in the description of an Enum Option, for example.
  • Paragraphs: In the right context, paragraphs are fine, using a blank like.
  • Block-Quotes, Block Code: Not yet
  • Lists: Use sparingly in the right context, nest freely
  • Tables: Not yet, except for the special emum case.

Links... Are going to be a whole thing. Linking to wikipedia is fine, but the path structure relative to the docs is not yet defined.

A special syntax similar to autolink for element links is pending, so when we refer to an [Foo] in a [Bar] we can link to the definition of Foo automatically.

Sequence and State Diagrams in mermaid are coming, probably more directly than the documentation comments.

Layer 2: Schema

Status: Work in progress.

Layer 3: Modules

Status: Future.

The language extensions use ., so are compatible with macros in UCL syntax helpers.

.partial and .include

Defines a partial entity, which can be merged into another entity of the same type.

.partial field cusip {
    required
    validate.regex = "^[A-Z0-9]{9}$"
}

field foo string {
    include cusip
}

field bar string {
    .include cusip
    validate.regex = null
}

The resulting Foo is required with the regex.

Bar will still be required (as useless as that is) but will not have the regex.

There is no syntax to nullify a directive. // TODO: 'unset' directive?

.import and .export

Imports all exported elements from another file under the given namespace.

.import foo.bar
.import foo.bar as baz

The exported elements are available as bar.element or baz.element respectively, rather than requiring the full namespace to be repeated.

Exports all elements for use by other namespaces. By default, elements defined within a namespace are available to that namespace and its children, but not to other namespaces.


// namespace/foo.j5
object foo {
  .export
  field bar string
}

partial object baseline {
  field createdAt timestamp
}

// other/bar.js
.import namespace.foo as baz

object qux {
  .include baz.baseline

  field bar_ref {
    ref baz.foo
  }
}

Directories

Path Synopsis
bcl
errpos
package errpos provides position/syntax and context path based errors, compatible with linters and humans.
package errpos provides position/syntax and context path based errors, compatible with linters and humans.
cmd
bcl
gen
internal
lsp
This file was copied from https://raw.githubusercontent.com/vito/bass/main/pkg/lsp/lsp.go
This file was copied from https://raw.githubusercontent.com/vito/bass/main/pkg/lsp/lsp.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL