1 Declarative Web Framework Design
Michael Zhang edited this page 2024-10-17 23:49:01 +00:00

Declarative Web Framework

The eventual goal of this is to create a language that compiles down to files, in a very similar way to Nix. The idea is to have a pure language that describes an impure computation.

  • Some kind of type that represents "files" on disk. This is the compile output
    • Need a way of saying "generate me a path during compile-time" in a way that all of the downstreams still refer to the same path
      • Could be a hash based on the file:span of the invocation?
    • Lots of tools for performing string parsing and editing
  • A higher-level "resource" that may compile to files.
    • A resource consists of:
      • Arbitrary attribute set, that can include other sub-resources
      • Recommended to only have one of the following two:
        • A set of "default" sub-resources to include
        • A set of files to emit
      • A function that operates over and modifies other resources
    • Example: a database schema may be a higher-level resource. Compiling this should result in some database files
      • To perform migrations, the migration CLI tool that's generated would write a file indicating the new state of the database it just learned back into the source repo to be checked in.
      • I think the type signature should look something like this:
        • DatabaseTable : {
            name: String,
            files: Set<File>,
            // Resources is essentially a set of files along with some particular fields of interest
            // Here, apply is allowed to assume that all the files in the files field exist
            apply: Resources -> Resources
          }
          
  • A concept of a "singleton" resource
    • I think this would mostly be for the post-compilation step, where including for example any DocumentationResource should trigger a post-compilation collection of all documentation resources
  • Quoting for various source code languages like Javascript

Stages of compilation

  1. A non-Turing complete step is used to collect everything into a flat set of resources
  2. An optional post-compilation step is run, consisting of all the PostCompilationResource

Example components

Auth plugin

let db : SQLDatabase<Sqlite> = ...
let adaptor : SQLWithDb<_> = Adaptor({ db })
let authPlugin = AuthPlugin({
  backend: ...  // some kind of config for a database adaptor
                // one example would be "SQL db w/ password"
                // another could be "SQL db w/ magic link"
  clientStrategy: ... // some config like session cookie or jwt
  allowGuests: true,
})

authPlugin would be {
  base: Resource,
  usersTable: SQLTableResource,
  loginRoute: (LoginRouteOpts) -> EndpointResource,
  loginComponent: (LoginComponentOpts) -> WebComponentResource,
  registerRoute: (RegisterRouteOpts) -> EndpointResource,
  registerComponent: (RegisterComponentOpts) -> WebComponentResource,
}

Comments plugin

let commentsPlugin = CommentsPlugin({

})

result {
  base: Resource,
  component: (CommentComponentOpts) -> WebComponentResource,
}

Example endpoint

let submitFlagComponent = ({}) => {
  let endpoint = EndpointResource({
    guards: {
      user: UserAuthGuard(),
      flags: DbTableGuard(flagTable),
      flagChecker: FlagCheckerGuard(user),
      submittedFlag: PostJsonFieldGuard("flag", "string"),
    },
    // This entire next part emits an AST, it does NOT actually run this code
    action: do {
      result <- flagChecker(submittedFlag);
      flags.insert({
        userId: user.id,
        flag: submittedFlag,
        correct: result.correct,
      });
      return result;
    }
  })
  let inputBox = WebComponentResource(jsxQuote!(
    ({ postSubmit }) => {
      const error = useState();
      const onSubmit = () => {
        const result = await fetch(#{endpoint.url}, { method: "POST" });
      };
      return <form onSubmit={onSubmit}>
        <input placeholder="Submit flag..." />
      </form>;
    }
  ))
  Resource.bundle([ endpoint, inputBox ])
}

Deploy to kubernetes plugin

This would be a function that takes a resource consisting of a bunch of endpoints, database tables, etc. and produces a deploy script

Deploy to docker compose plugin

Run a post-compile script that produces a docker image with a particular image name, as well as a docker-compose.yml file

Documentation

There should be a "documentation resource" type, which gets collected all together with a search index

Open questions

  • Need something like multi-stage programs? Functions annotated with @stage(n). Can't imagine a situation needing this yet