Programmers of the world, I want your input on this.

I have noticed the same pattern across many modern libraries: first define a lot of whatever (routes, commands, UI components, callbacks, cli flags) using some nice functions/methods/builder-pattern into some internal data-structure and then having some parse(), render(), serve(), or run() using that data-structure and doing stuff with it

so one of the ideas with MiZe is to make a good library around this pattern. I guess the goal here is to build an alternative to Qt (gui) or GTK (gui)…

note: this is quite inspired by the NixOS Module System. where each module can define options and also set them. and the evaluation of some top_level produces a NixOS system.

the library tracks get_config, get_part calls inside pure_init functions and options’s functions to learn what depends on what and builds a graph. all options which can be serialized can be done so and saved to disk for faster application startup. library only reruns what actually needs to whenever a option changes. get_config and get_part also block/yield until the option is available. with this I want to build a systemd alternative.

three things I am specifically curious about

  • what examples programming logic/application can you come up with which does not play well with this pattern at all?
  • what problems do you see with this approach/what do you hate about it?
  • any ideas on how to make the opt names more integrated into the language as symbols?
    • so that you could naively jump to the definition of an option…
    • but without requiring any MetaObjectCompiler like Qt
    • maybe a mechanism to generate src is fine (as that will be part of MiZe anyway)

matrix room for questions and discussion: https://matrix.to/#/#config-oriented-programming:ppc.social

spacetimedb part

#[part("spacetime")]
pub fn pure_init() -> MizeResult<()> {
  // regular config options
  define("is_https", user_required<bool>)?;
 
  // you can define how options are calculated from other options
  define("url", || {
    let domain = get_config("domain")?;
    if get_config("self.is_https")? {
      Ok(format!("https://{domain}"))
    } else {
      Ok(format!("http://{domain}"))
    }
  })?;
  // this works both ways (meaning one of the two has to be given)
  define("domain", || {
    parse_from_url(get_config("self.url")?)
  })?;
  define("db_name", user_required<String>)?;
 
  // config options can have language specific types. and you are supposed to use it also for stuff like a db_connection
  define("connection", || {
    let url = get_config("self.url")?;
    let db_name = get_config("self.url")?;
    // you can simply return an Err here and it will by default retry... or fail all "config options" after it.
    return Ok(Spacetime::connection_builder().url(url).db_name().build())?;
  })?;
  
  // the function which is run once all config has resolved ("it applies the state of config to the real world"). in it's own thread (or an async task), usually running forever.
  // simply rerun if any config it get_config's changes
  define("run", || {
    let con = get_config("self.connection")?;
    con.on_updates_in_table("test", | update |, println!("got update: {:?}", update));
  })?;
 
  define(method("get_table"), |name: String| {
    // get config also handles blocking until self.connectin is ready.
    let con = get_config("self.connection")?;
    return Ok(con.table(name));
  })?;
 
  // you can define dummy opts to run effects whenever smth changes
  define(dummy(), || {
    get_config("self.connections")
  })
 
}

web

#[part("web.server")]
pub fn pure_init() -> MizeResult<()> {
  // generates methods add_route(), list_routes(), remove_route(), get_route(), .....
  define(generate_vec_methods("routes"), || {
    Ok(Vec::new::<axum::Route>())
  })?;
 
  define(with_env("port", "PORT").default(3000), || {})?;
  
  define("run", || {
    axum::server(get_config("self.routes"), get_config("self.port"))
  })?;
 
  Ok(())
}

small example application using this

#[part("c2vi.website")]
pub fn pure_init() -> MizeResult {
	let web = get_part("web.server")?;
	web.add_route(GET, "/", my_main_route_fn)?;
	web.add_route(GET, "/now", || {
		let web = get_part("web.server")?;
		let spacetime = get_part("spacetime")?;
		let user = web.get_user()?;
		return spacetime.get_table("now").by_id(user)
	})?;
}

web with builder pattern

#[part("web.server")]
pub fn pure_init() -> MizeResult<()> {
  opt("routes")
    .generate_vec_methods()
    .value(Vec::new::<axum::Route>());
 
  opt("port")
    .env_var("PORT")
    .default(3000);
  
  opt("run")
    .fn(|| {
      axum::server(get_config("self.routes"), get_config("self.port"))
    });
  
  Ok(())
}

web in C (if the patter works in C, it will work everywhere)

mize_result web_run() {
  run_server(get_config("self.routes"), get_config("self.port"));
}
 
PART_PURE_INIT("web.server") web_server_pure_init() {
  OPT routes = opt("routes");
  generate_vec_methods(routes);
  value(routes, mize_vec_new());
  
  OPT port = opt("port");
  env_var(port, "PORT");
  default(port, 3000);
  
  OPT run = opt("run");
  fn(run, web_run);
}

a toml config file also becomes a part (which simply sets values for other options)

[web.server]
port = 3000
 
[spacetime]
url = "https://spacetime.ppc.social"
auth_issuer = "https://auth.ppc.social"

aka:

#[part("app_config")]
pub fn pure_init() -> MizeResult<()> {
	set_config("web.server.port", 3000)?;
	//and so on
}