Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/facade/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,42 @@ entries:
path: /index.html
func: index_handler

# Optional view-config resolution contract (bound by the application, if at all)
- name: resolver
kind: contract.definition
meta:
comment: Optional view-config resolution contract — override facade requirement values at request time
description: |
Optional contract for overriding facade requirement values (css_variables,
custom_css, app_title, app_icon, theme_mode, ...) at request time. An
application binds an implementation to drive the view config from runtime
state (settings, per-tenant branding). When no binding exists, the facade
config and CSS-vars handlers fall back to the static ns.requirement defaults
— detected structurally via :implementations() (an empty list), not by
inspecting the contract open error. Runs under the request's ambient actor.
methods:
- name: resolve
description: |
Return a map of facade requirement name -> override value (string). Values
use the same format as the static requirement default (JSON-encoded for the
JSON requirements such as css_variables). Omitted keys fall back to the
static default.
input_schemas:
- definition: |
{
"type": "object",
"description": "Optional resolution context (reserved; empty today)."
}
format: application/schema+json
output_schemas:
- definition: |
{
"type": "object",
"description": "Map of facade requirement name to override value string; omitted keys fall back to the static ns.requirement default.",
"additionalProperties": { "type": "string" }
}
format: application/schema+json

# Shared helpers: file:// resolution and CSS variable generation
- name: theming_helpers
kind: library.lua
Expand All @@ -495,6 +531,8 @@ entries:
modules:
- fs
- json
- registry
- contract

# Config endpoint
- name: config_handler
Expand Down
79 changes: 38 additions & 41 deletions src/facade/config_handler.lua
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
local http = require("http")
local registry = require("registry")
local json = require("json")
local env = require("env")
local theming = require("theming_helpers")

local NS = "wippy.facade:"

local function get_req(name: string): string
local entry, _ = registry.get(NS .. name)
if entry and entry.data then
return entry.data.default or ""
end
return ""
end

local function get_req_json_any(name: string): {[string]: any}
local raw = get_req(name)
if raw == "" then
return {}
end
local val, err = json.decode(raw)
if err then
return {}
end
return (val :: {[string]: any}) or {}
end

local function derive_ws_url(api_url: string): string
if api_url == "" then
return ""
Expand Down Expand Up @@ -54,30 +31,50 @@ local function non_empty_array_or_nil(a: any): {any}?
return a :: {any}
end

-- Build a theming scope from requirement name prefixes.
-- file_sys is optional: when set, "file://path" values are resolved to file content.
local function build_theming_scope(css_req: string, vars_req: string, icon_sets_req: string?, file_sys: any): {[string]: any}?
local custom_css = non_empty_or_nil(theming.resolve_css(get_req(css_req), file_sys))
local css_vars = non_empty_map_or_nil(theming.resolve_json(get_req(vars_req), file_sys) :: {[string]: any})
-- icon_sets are always inline JSON blobs; file:// refs are not expected here
local icon_sets = icon_sets_req and non_empty_map_or_nil(get_req_json_any(icon_sets_req)) or nil

if not custom_css and not css_vars and not icon_sets then
return nil
end
return {
customCSS = custom_css,
cssVariables = css_vars,
iconSets = icon_sets,
}
end

local function handler()
local res = http.response()
if not res then
return nil, "no HTTP context"
end

-- Optional view-config resolver (wippy.facade:resolver): when an application
-- binds one, its overrides replace matching requirement values below; with no
-- binding this is {} and every read falls back to the static ns.requirement.
local overrides = theming.resolve_overrides()

local function get_req(name: string): string
return theming.requirement(name, overrides)
end

local function get_req_json_any(name: string): {[string]: any}
local raw = get_req(name)
if raw == "" then
return {}
end
local val, err = json.decode(raw)
if err then
return {}
end
return (val :: {[string]: any}) or {}
end

-- Build a theming scope from requirement name prefixes. file_sys is optional:
-- when set, "fs://path" values are resolved to file content.
local function build_theming_scope(css_req: string, vars_req: string, icon_sets_req: string?, file_sys: any): {[string]: any}?
local custom_css = non_empty_or_nil(theming.resolve_css(get_req(css_req), file_sys))
local css_vars = non_empty_map_or_nil(theming.resolve_json(get_req(vars_req), file_sys) :: {[string]: any})
-- icon_sets are always inline JSON blobs; fs:// refs are not expected here
local icon_sets = icon_sets_req and non_empty_map_or_nil(get_req_json_any(icon_sets_req)) or nil
if not custom_css and not css_vars and not icon_sets then
return nil
end
return {
customCSS = custom_css,
cssVariables = css_vars,
iconSets = icon_sets,
}
end

local facade_url = get_req("fe_facade_url")
local entry_path = get_req("fe_entry_path")
local fe_mode = get_req("fe_mode")
Expand Down
19 changes: 8 additions & 11 deletions src/facade/css_vars_handler.lua
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
local http = require("http")
local registry = require("registry")
local theming = require("theming_helpers")

local NS = "wippy.facade:"

local function get_req(name: string): string
local entry, _ = registry.get(NS .. name)
if entry and entry.data then
return entry.data.default or ""
end
return ""
end

local function handler()
local res = http.response()
if not res then
return nil, "no HTTP context"
end

-- Optional view-config resolver (wippy.facade:resolver): overrides win over the
-- static requirement, so this CSS endpoint tones from the same source as
-- /facade/config. No binding => {} => static defaults.
local overrides = theming.resolve_overrides()
local function get_req(name: string): string
return theming.requirement(name, overrides)
end

local file_sys = theming.get_fs(get_req("content_fs"))
local vars = theming.resolve_json(get_req("css_variables"), file_sys)

Expand Down
58 changes: 58 additions & 0 deletions src/facade/theming_helpers.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
local fs = require("fs")
local json = require("json")
local registry = require("registry")
local contract = require("contract")

local helpers = {}

local NS = "wippy.facade:"
-- Optional contract an application binds to override facade requirement values at
-- request time (see the `resolver` contract.definition). When unbound, open() fails
-- and every read falls back to the static ns.requirement default.
local RESOLVER_CONTRACT = "wippy.facade:resolver"

-- Get a named filesystem instance by registry entry ID.
-- Returns nil (does not raise) when entry_id is empty or the named filesystem
-- does not exist, so callers degrade gracefully when content_fs is unconfigured.
Expand Down Expand Up @@ -149,4 +157,54 @@ function helpers.build_variables_css(vars: {[string]: any}): string
return table.concat(css_parts, "\n")
end

-- resolve_overrides: optionally consult a bound wippy.facade:resolver to override
-- facade requirement values at request time. Returns a name->value(string) map, or
-- {} when no resolver is bound (the common case) or resolution fails. The view must
-- always render, so any failure degrades to the static requirement defaults; an
-- absent resolver (no binding) is silent, a bound-but-failing one is logged.
function helpers.resolve_overrides(): {[string]: any}
-- Any failure degrades to {} (static defaults) so the load-bearing config/CSS
-- endpoints never break. open() alone suffices: it fails cleanly when unbound,
-- and unbound vs errored both fall back to static, so nothing gates the open.
-- Ambient actor/scope; pcall guards against unexpected throws.
local ok, result = pcall(function(): {[string]: any}
local c, get_err = contract.get(RESOLVER_CONTRACT)
if get_err or not c then
return {}
end
local instance, open_err = (c :: any):open()
if open_err or not instance then
return {}
end
local overrides, resolve_err = (instance :: any):resolve({})
if resolve_err or type(overrides) ~= "table" then
return {}
end
return overrides :: {[string]: any}
end)
if not ok then
return {}
end
return result :: {[string]: any}
end

-- requirement: the value for a facade requirement, preferring a resolver override
-- (a string) over the static registry default. Mirrors the handlers' get_req so a
-- resolver can replace any requirement (css_variables, custom_css, app_title, ...).
-- Override values use the same format as the static default (JSON-encoded for the
-- JSON requirements such as css_variables); omitted keys fall back to the default.
function helpers.requirement(name: string, overrides: {[string]: any}?): string
if overrides then
local ov = (overrides :: {[string]: any})[name]
if type(ov) == "string" then
return ov :: string
end
end
local entry, _ = registry.get(NS .. name)
if entry and entry.data then
return entry.data.default or ""
end
return ""
end

return helpers
Loading