neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기docuconf for Elixir, for config/runtime.exs.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Typed configuration contracts for Elixir applications, from the docuconf specification (v1alpha1).
Documentation: docuconf.dev · Elixir guide
Elixir apps read their runtime configuration in config/runtime.exs with
System.fetch_env!/1 and System.get_env/2. docuconf keeps that file and
adds what it lacks:
/dev/termination-log so kubectl describe pod shows it;mix docuconf.export, which writes contract.cue, a CUE document the
platform validates before it deploys anything.There are no runtime dependencies. JSON comes from Elixir 1.18's JSON
module and certificate handling from OTP's :public_key.
examples/orders is a small HTTP service built with it.
The quickstart below goes install, declare, run, see an error, test, export,
deploy. Phoenix has a drop-in runtime.exs, and the
reference follows.
docuconf is not on Hex yet. Until the first release, depend on it from
GitHub in mix.exs:
def deps do
[
{:docuconf, github: "docuconf/docuconf-elixir", branch: "main"}
]
end
then run mix deps.get. A local checkout works the same way with
{:docuconf, path: "../docuconf-elixir"}. Once 0.1.0 is published the
dependency becomes {:docuconf, "~> 0.1"}.
Requires Elixir 1.18 or later, on OTP 25 or later.
One module lists everything the app reads from its environment:
defmodule MyApp.Env do
use Docuconf, name: "my-app"
env :port, :pos_integer, description: "HTTP listen port", default: 4000, max: 65535
env :log_level, {:in, [:debug, :info, :warning, :error]},
description: "Minimum log level",
default: :info
env :request_timeout, :duration, description: "Upstream request timeout", default: "30s"
secret :database_url, :url,
description: "Primary Postgres connection string",
required: true,
schemes: ["postgres", "ecto"]
end
The name of each variable is its field upcased (:database_url reads
DATABASE_URL). The declaration is checked when the module compiles: an
unknown option, a default that breaks its own constraints or a pattern RE2
cannot run is a compile error that points at the line, with a suggestion
(unknown option :maximum for :integer; did you mean :max?).
The module gets a struct with a typed field per input (@type t, so
Dialyzer and your editor know log_level is :debug | :info | ...),
generated docs listing every variable, and load/1, load!/1 and
export/1.
Load it in config/runtime.exs:
import Config
if config_env() != :test do
env = MyApp.Env.load!(dotenv: config_env() == :dev && ".env")
config :logger, level: env.log_level
config :my_app, MyApp.Repo, url: env.database_url
config :my_app, :request_timeout, env.request_timeout
end
env.log_level is an atom, env.request_timeout is 30_000 (milliseconds,
ready for OTP timeouts) and env.port an integer. In development, put
values in a .env file (DATABASE_URL=postgres://localhost/my_app_dev);
real environment variables override it, and outside :dev the expression
is false, so no file is read. Tests load the declaration themselves (see
step 5).
inspect(env) (and so IEx, Logger and crash reports) shows
database_url: **redacted**. Storing the struct with
config :my_app, env: env is fine for inspection, but remember that
anything in the application environment can be read with
Application.get_env/2.
A bad environment stops the boot with every problem listed, and nothing else:
PORT=0 mix run
docuconf: 2 configuration problems:
- DATABASE_URL [missing_required]: required, but not set
- PORT [out_of_range]: "0" is below min 1
The process exits with status 1, with no stack trace and, in a release, no
erl_crash.dump. The same lines go to /dev/termination-log inside a
container. Secret values never appear: a wrong DATABASE_URL shows its
scheme (scheme "mysql" is not one of postgres, ecto), never the URL.
Typos get a hint. If DATABSE_URL is set, boot also prints
docuconf: warning: DATABSE_URL is set but not declared; did you mean DATABASE_URL?
(a warning, never the value).
load/1 and load!/1 take an explicit environment with env:, so a test
never reads or changes the process environment. With env:, load!/1
raises Docuconf.ValidationError instead of stopping the node, and no
termination log is written.
# test/my_app/env_test.exs
defmodule MyApp.EnvTest do
use ExUnit.Case, async: true
@valid %{"DATABASE_URL" => "postgres://app:secret@localhost/app"}
test "defaults apply" do
env = MyApp.Env.load!(env: @valid)
assert env.port == 4000
assert env.log_level == :info
assert env.request_timeout == 30_000
end
test "every problem is reported together" do
assert {:error, error} = MyApp.Env.load(env: %{"PORT" => "0"})
assert Enum.map(error.violations, &{&1.input, &1.code}) == [
{"DATABASE_URL", :missing_required},
{"PORT", :out_of_range}
]
end
test "secrets are not shown" do
env = MyApp.Env.load!(env: @valid)
refute inspect(env) =~ "secret@"
end
end
File inputs are read under file_root: (MyApp.Env.load!(env: ..., file_root: tmp_dir)),
so tests can lay out /etc/... mounts in a temporary directory.
Export the contract and commit it, or publish it next to the image:
mix docuconf.export MyApp.Env # writes contract.cue
mix docuconf.export MyApp.Env --check # in CI: fails if contract.cue is stale
mix docuconf.export MyApp.Env -o - # to stdout, nothing else
In a fresh CI job, run mix deps.compile before -o -: Mix compiles
docuconf itself, and says so on stdout, before the task exists to silence it.
Export never needs the environment. Set the module once in mix.exs
(docuconf: [module: MyApp.Env]) to run plain mix docuconf.export.
--check prints the difference and the exact command that fixes it, and
ignores appVersion and the SDK version, so bumping version in mix.exs
does not fail CI.
Ship contract.cue with the image. The platform validates its values
against it before anything runs: docuconf vet checks a deployment's values
and docuconf render turns them into the Kubernetes env and Secret
references, or a Helm-based platform uses the
docuconf Helm chart.
A missing DATABASE_URL is then caught at deploy time, and the boot check
is the last line of defence. In a release, runtime.exs runs at boot, so
the same clean failure and termination log apply.
A Phoenix app generated by mix phx.new reads its environment in
config/runtime.exs, inside if config_env() == :prod. Declare the same
variables:
defmodule MyApp.Env do
use Docuconf, name: "my-app"
env :phx_server, :boolean,
description: "Start the endpoint (set to true by rel/overlays/bin/server)",
default: false
env :phx_host, :string, description: "Host name in generated URLs", default: "example.com"
env :port, :pos_integer, description: "HTTP listen port", default: 4000, max: 65535
secret :secret_key_base, :string,
description: "Cookie signing key (mix phx.gen.secret)",
required: true,
min_length: 64
secret :database_url, :url,
description: "Ecto database URL",
required: true,
schemes: ["ecto", "postgres", "postgresql"]
env :pool_size, :pos_integer, description: "Database connection pool size", default: 10
env :ecto_ipv6, :boolean, description: "Connect to the database over IPv6", default: false
env :dns_cluster_query, :string, description: "DNS query that finds the other nodes"
end
and replace the generated runtime.exs with:
import Config
if config_env() != :test do
# In dev, values that do not belong in the contract (a dev cookie key, the
# local database) come from fallback_env; real variables win.
env =
MyApp.Env.load!(
fallback_env:
config_env() == :dev &&
%{
"SECRET_KEY_BASE" => String.duplicate("dev-only-key-", 5),
"DATABASE_URL" => "ecto://postgres:postgres@localhost/my_app_dev"
}
)
if env.phx_server do
config :my_app, MyAppWeb.Endpoint, server: true
end
config :my_app, MyApp.Repo,
url: env.database_url,
pool_size: env.pool_size,
socket_options: if(env.ecto_ipv6, do: [:inet6], else: [])
config :my_app, MyAppWeb.Endpoint,
http: [port: env.port],
secret_key_base: env.secret_key_base
config :my_app, :dns_cluster_query, env.dns_cluster_query
if config_env() == :prod do
config :my_app, MyAppWeb.Endpoint,
url: [host: env.phx_host, port: 443, scheme: "https"],
http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}]
end
end
Now mix phx.server validates in dev too, with no .env needed (add
dotenv: config_env() == :dev && ".env" if you keep local values in one),
and a
release with a bad environment fails at boot with the problem list.
config/dev.exs keeps its other settings; http: [port: ...] merges with
its ip:. Two differences from the generator's code:
PHX_SERVER is a boolean: true or false (the generated
bin/server sets PHX_SERVER=true). PHX_SERVER=1 is rejected as
"1" is not true or false, where the generator accepted any value.runtime.exs (config/test.exs already has everything) and
load the declaration with env: as in step 5.Run mix docuconf.export in CI as in step 6. This is a
recipe rather than a Phoenix-specific module: everything happens in
runtime.exs, which Phoenix already owns, and the recipe is run in this
repository's tests (test/readme_test.exs) for dev, prod and the
PHX_SERVER switch.
env field, type, opts declares a variable. secret is env with
secret: true.
| Type | Contract type | Value | Options |
|---|---|---|---|
:string | string | String.t() | min_length, max_length, pattern |
:integer | int | 64-bit integer | min, max |
:pos_integer, :non_neg_integer | int | integer, min: 1 or min: 0 | min, max |
:float | float | float (NaN/Inf rejected) | min, max |
:boolean | bool | true/false, case-insensitive | |
:duration | duration | integer in unit | min, max, unit, encoding |
:url | url | string with scheme:// | schemes, max_length |
{:in, values} | enum | an atom if every value is an atom, else a string | |
{:list, :string}, {:list, :integer} | list | list | encoding, separator (default ,), min_items, max_items; item_min, item_max (integer lists); item_min_length, item_max_length (string lists) |
:key_set | keySet | Docuconf.KeySet (always secret) | encoding, separator (default ,), min_keys (default 1), (default 2), , |
Every variable takes description (or doc; at least 5 characters, required
unless an @doc gives it), details (see below), required, default, secret, group, examples, deprecated
(a message, or [message: ..., replaced_by: :new_field]; the message is not
blank and at most 500 characters, and a required input cannot be
deprecated), config_key, name and flag_warning. A deprecated input
that is set still loads and is still checked; at boot docuconf warns,
naming the input and its message, never the value:
docuconf: warning: OLD_PORT is deprecated (replaced by PORT): Use PORT instead.
1m30s,
250ms, 1.5h), parsed by docuconf itself because Elixir has no
standard duration string, and are written to the contract in canonical Go
form. unit: picks what the app gets: :millisecond (the default,
matching OTP timeouts), :second, :microsecond, :nanosecond, or
:duration for an Elixir Duration. A value that is not a whole number
of the unit is rejected. default, min and max take a Go string
("30s"), the variable's own encoding ("PT30S" with encoding: :iso8601),
an integer in the unit (30_000, :timer.seconds(30)) or an Elixir
Duration (Duration.new!(second: 30)).:csv (the default, joined by separator),
:json (["a","b"]) or :indexed (NAME__0, NAME__1, ...; items
must be numbered from 0 with no gap, or the list is invalid_type).
Durations: :go (the default), :iso8601 (PT1M30S), :seconds (90)
or :timespan (00:01:30). A value in the wrong form gets the right one
in its error: "30s" is not an ISO 8601 duration such as PT1M30S; write PT30S.^ and $. A ~r sigil or a string both work. PCRE-only
features (lookaround, backreferences, atomic groups, possessive
quantifiers) are rejected at compile time. Matching follows RE2, not
PCRE: $ means end of text (not "before a final newline"), and \d,
\w, \s and \b are ASCII-only.item_min and item_max bound each item of a
{:list, :integer} and are exported as itemMin and itemMax. Every
item is already checked against the 64-bit range of the contract's int.The declaration is checked when the module compiles: name format,
description length, defaults against their own constraints, required or
secret with a default, RE2-only patterns, unknown or misplaced options,
non-boolean required/secret, and file mount rules. Every problem is
listed in one Docuconf.DeclarationError, each with its file:line.
Calling env in a module without use Docuconf is a compile error. Names
that look like feature flags (FF_, FEATURE_, ENABLE_) get a compile
warning on their line (SPEC §10); flag_warning: false on that declaration
silences it for a deploy-time switch.
A :key_set is a set of secret keys that are all valid at once, so a key
can be rotated without an outage (SPEC §4.3, §6.1): during a rotation the
platform sets old,new. It is always secret. Its value is a
Docuconf.KeySet: Docuconf.KeySet.keys/1 gives the keys in order,
contains?/2 compares a candidate (an inbound API key) with every key in
constant time, and verify?/2 runs your check, such as an HMAC comparison,
against every key without stopping at the first match:
defmodule MyApp.Env do
use Docuconf, name: "payments"
@doc "Keys that verify webhook signatures"
secret :webhook_keys, :key_set, key_min_length: 32
end
defmodule MyApp.Webhook do
def valid?(keys, body, signature) do
Docuconf.KeySet.verify?(keys, fn key ->
expected = :crypto.mac(:hmac, :sha256, key, body) |> Base.encode16(case: :lower)
byte_size(expected) == byte_size(signature) and :crypto.hash_equals(expected, signature)
end)
end
end
More keys than max_keys (default 2) or fewer than min_keys (default 1)
is too_many_items or too_few_items; a key outside
key_min_length..key_max_length, and an empty key whatever the bounds
(a stray comma), is out_of_range. No message, inspect/2 or
to_string/1 ever shows a key. docuconf docs prints the rotation steps,
so details need not repeat them.
Document an input with @doc, as you would a function. Its first paragraph
is the contract's description (on one line, without the final period) and
the rest is details: Markdown for generated docs, at most 4000 characters,
never read at runtime.
defmodule MyApp.DocumentedEnv do
use Docuconf, name: "my-app"
@doc """
Upstream request timeout.
Raise it for clients that upload large batches. Keep it below the load
balancer's idle timeout, or the client sees a reset rather than a `504`.
"""
env :request_timeout, :duration, default: "30s"
env :pool_size, :pos_integer,
description: "Database connection pool size",
details: "One connection per scheduler is a good start.",
default: 10
end
The description: and details: options set either one explicitly and win
over the @doc, which env, secret and the file declarations consume, so
it never documents the next function. ExDoc-only syntax becomes CommonMark:
auto-link prefixes (`m:Mod`, `t:Mod.t/0`) are dropped,
[text](`Mod.fun/1`) links become code spans and {: .info} attributes are
removed. A missing or short description, and details that are blank or longer
than 4000 characters, fail compilation. docuconf docs in the
docuconf CLI generates CONFIG.md
and CONFIG.agents.md from the exported contract.
| Macro | Contract type | data after loading | Extra options |
|---|---|---|---|
config_file | config | decoded document (atoms with a keyword spec) | format (:json, :yaml, :toml), schema, decoder |
tls_file | tls | %{certfile, keyfile, cacertfile, certificate, chain, cacerts, not_after} | dns_names, key_algorithms (:rsa, :ecdsa, :ed25519), min_remaining, require_ca |
ca_bundle_file | caBundle | list of DER certificates (for cacerts:) | min_certificates |
keystore_file | keystore | nil | format (:pkcs12, :jks), password_var |
text_file | text | the content | pattern, min_length, max_length |
binary_file | binary | nil |
defmodule MyApp.Files do
use Docuconf, name: "orders"
config_file :pricing,
format: :json,
description: "Pricing rules: currency and discount tiers",
required: true,
path: "/etc/orders/pricing/pricing.json",
schema: [
currency: [type: :string, required: true, pattern: "^[A-Z]{3}$"],
tiers: [type: {:list, {:map, [min_total: [type: :pos_integer, required: true]]}}]
]
tls_file :serving_tls,
description: "Certificate the API serves HTTPS with",
required: true,
path: "/etc/orders/tls",
dns_names: ["orders.internal"],
min_remaining: "720h"
end
All take description (or an @doc, as for variables), details, path
(absolute), path_env, required, secret, reload (:restart or
:watch), max_size, group and deprecated. The
input name is the field with _ replaced by - (:serving_tls is
serving-tls). A loaded file is a %Docuconf.LoadedFile{path, data}; an
absent optional file is nil. A secret config or text file's data is
shown as **redacted** when inspected.
DOCUCONF_FILE_ROOT (or file_root:) is prefixed to
every absolute path, including paths read from a path_env variable, for
local development and tests. A missing file's error says so.decoder: (a remote capture returning {:ok, data}): docuconf's own
&Docuconf.TOML.decode/1 (all of TOML 1.0) or &Docuconf.YAML.decode/1
(the YAML config files use, with the core schema; anchors, tags and
several documents are errors), or a library's, such as
&YamlElixir.read_from_string/1. JSON needs nothing.:public_key: the key matches the certificate (by
signing and verifying a probe), the certificate is valid now with at least
min_remaining left, :public_key.pkix_verify_hostname/3 covers every
name in dns_names (a wildcard covers one label), the key algorithm is
allowed, and with require_ca the chain in tls.crt validates against
ca.crt with :public_key.pkix_path_validation/3.password_var. SHA-1 and SHA-2 MACs are supported. A matching MAC proves
the password is right and the file is intact; the keys are not decrypted.
JKS and JCEKS stores are checked the same way. PBMAC1 MACs (OpenSSL 3.4
-pbmac1_pbkdf2) are not supported yet.reload: :watch puts a promise in the contract: the app rereads the file
itself, so the platform does not restart the pod when it changes. Keep it by
running Docuconf.Watcher in your supervision tree, before the processes
that use the files:
defmodule MyApp.Files do
use Docuconf, name: "orders"
tls_file :serving_tls,
description: "Certificate the API serves HTTPS with",
path: "/etc/orders/tls",
dns_names: ["orders.internal"],
reload: :watch
ca_bundle_file :partner_ca,
description: "CAs that sign the partner API's certificate",
path: "/etc/orders/partner-ca/ca.crt",
reload: :watch
end
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Docuconf.Watcher, module: MyApp.Files},
MyApp.Endpoint
]
Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Supervisor)
end
end
It polls in the background (OTP has no portable file-event API; every 5
seconds, or interval: milliseconds), runs the file's boot checks again on
every change, and swaps in the new value only if they pass. A change that
fails them is logged, and the previous value stays current. It reads the
environment the way the module's last load did, .env and file_root
included. If you do not run it, declare reload: :restart, which is the
default.
For each watched input, Docuconf.Watcher gives you:
current(module, field): the Docuconf.LoadedFile in use now, from an
ETS table, so it is cheap to call on every request;subscribe(module, field): the calling process receives
{:docuconf_reloaded, module, field, %Docuconf.LoadedFile{}} after each
accepted reload, until it exits or calls unsubscribe/2;on_change(module, field, fun): fun is called with the new
Docuconf.LoadedFile after each accepted reload, in the watcher process.
It returns a reference for unsubscribe(module, ref). The watcher's own
on_change: fn field, file -> ... end option is one more such hook;status(module, field) and status(module): the Docuconf.ReloadStatus.Hooks and messages fire from the background poll, without a read, after an
accepted reload only, never for a rejected change. Several are allowed, and
run one at a time in the order they were added. A hook that raises, throws
or exits is logged by input name and error type, never its message; the
other hooks still run and the reload stands. Hooks belong to the watcher
process, so if it restarts they must be added again: a :rest_for_one
supervisor, as above, restarts the processes started after it, which then
subscribe again.
A value copied once at startup, into a TLS listener's options, an HTTP
client or a pool, never sees a reload, and a renewed certificate is mounted
but never served until the old one expires. Read current/2 on each use,
or rebuild what you made from the value in a hook.
A TLS server configured with certfile: and keyfile: (Phoenix's https:
options, Bandit, Cowboy, :ssl.listen/2) reads those paths through OTP's
PEM cache. Clearing it in a hook makes the next handshake read the renewed
files; open connections keep the certificate they started with:
defmodule MyApp.TLSReload do
require Logger
# Call from MyApp.Endpoint's init/2, or a process started after the watcher.
def watch do
Docuconf.Watcher.on_change(MyApp.Files, :serving_tls, fn tls ->
:ssl.clear_pem_cache()
Logger.info("serving certificate renewed, valid until #{tls.data.not_after}")
end)
end
def https_options do
tls = Docuconf.Watcher.current(MyApp.Files, :serving_tls)
[certfile: tls.data.certfile, keyfile: tls.data.keyfile]
end
end
An HTTP client reads the CA bundle on each request:
defmodule MyApp.Partner do
def get(url) do
ca = Docuconf.Watcher.current(MyApp.Files, :partner_ca)
ssl = [verify: :verify_peer, cacerts: ca.data]
:httpc.request(:get, {String.to_charlist(url), []}, [ssl: ssl], [])
end
end
A client that keeps a pool (Finch, Mint, a Req instance) rebuilds it on
{:docuconf_reloaded, MyApp.Files, :partner_ca, ca} in the process that
owns the pool, after subscribe/2 in its init/1.
Docuconf.Watcher.status(module, field) returns a Docuconf.ReloadStatus:
generation (1 when the watcher starts at boot, plus one per accepted
reload), last_reload (the DateTime of the last accepted reload, or
nil) and last_rejected (a Docuconf.RejectedReload with the time,
the input name and the violation codes of the last change that failed
its checks, never the content; cleared by the next accepted reload).
status(module) returns a map of them by field. Both encode with JSON,
for a health check or a metric:
defmodule MyApp.Health do
# {"serving_tls":{"generation":2,"last_reload":"2026-10-10T13:44:18.526735Z","last_rejected":null},...}
def reloadz, do: JSON.encode!(Docuconf.Watcher.status(MyApp.Files))
end
The watcher reads the environment once, when it starts, so a keystore
reload opens the new keystore with the password read at boot (a running
process's environment does not change). Rotating a keystore's password
needs a rollout. A changed keystore that does not open with the boot
password is rejected as keystore_unreadable, and the previous one stays
current.
The promise is enforced. load!/1, when the declaration has a watch
input, checks once the application that owns the module has started that a
Docuconf.Watcher for the module is running. If not, it prints the problem
once, writes it to the termination log and stops the node with exit
status 1. Pass watcher_check: :warn to only log it, or
watcher_check: false to skip it. Loads with an explicit env: map skip
the check unless watcher_check is given. A module that belongs to no
application (a script) is checked after watcher_grace milliseconds
(default 5000).
MyApp.Env.load(opts) returns {:ok, env} or {:error, %Docuconf.ValidationError{}}.
MyApp.Env.load!(opts) returns the struct; on failure, reading the process
environment, it prints the problems and stops the node with exit status 1
(on_error: :halt), and with env: it raises (on_error: :raise). Options:
env: a map to read instead of the process environment (tests);dotenv: a .env file to read first, for development; real environment
variables override it. nil or false reads none. A named file that
does not exist is a warning;fallback_env: a map used only for variables nothing else sets (dev-only
values); nil or false uses none;file_root:, termination_log: (a path, or false), now: (a
DateTime for certificate checks), warn: false;on_error: :halt or :raise, for load!/1;watcher_check: and watcher_grace: (see Reloading files).Warnings go to standard error: a deprecated variable that is set, a secret
that ends in a newline (a common kubectl create secret --from-file
mistake), a missing .env file, and a set variable whose name is a likely
typo of a declared one.
Platforms often inject secrets into the environment at runtime: Bank-Vaults'
vault-env resolves vault: references, op run resolves op://, and vals
resolves ref+. docuconf reads the environment as the process sees it after
injection, so injected values are validated like any other, and it never
resolves a reference itself (SPEC §4.5.1). If the injector did not run, a
secret variable still holds the reference; docuconf reports that as
invalid_type, naming the scheme but never the value:
- DATABASE_URL [invalid_type]: holds an unresolved vault: reference; the injector that should resolve it did not run
A use Docuconf declaration has no overlays or profiles (SPEC §4.4,
§4.7). Elixir's config/*.exs files are compiled into the release and
config/runtime.exs is code, not a layered file stack, so there is nowhere
to put a platform-mounted overlay between the app's files and the
environment, and a value in config/prod.exs is an ordinary default:.
The exported contract never has either. Mount a config_file input instead
if the platform needs to supply structured configuration.
Contract-first mode does read both, for contracts
written for hosts that layer files.
missing_required, invalid_type, out_of_range, pattern_mismatch,
not_in_enum, invalid_scheme, too_few_items, too_many_items,
file_missing, file_unreadable, file_too_large, file_malformed,
schema_mismatch, certificate_invalid, certificate_expiring,
certificate_name_mismatch, key_mismatch, keystore_unreadable
(SPEC §11.2). Each Docuconf.Violation has input, kind, code and message.
Docuconf.Contract validates an environment against a contract given as
JSON (cue export contract.cue --out json), with no use Docuconf module,
for teams that write their contract in CUE by hand (SPEC §11.2 item 11):
contract = %{
"apiVersion" => "docuconf.dev/v1alpha1",
"kind" => "ConfigContract",
"metadata" => %{"name" => "orders"},
"vars" => %{"PORT" => %{"type" => "int", "description" => "HTTP listen port", "default" => 8080}}
}
{:ok, values} = Docuconf.Contract.load(contract, env: %{})
8080 = values["PORT"]
The contract is turned into the same declaration the DSL builds, so it gets
the same checks and parsers. Values come back as a
Docuconf.Contract.Values, keyed by variable or file input name: read them
with values["PORT"], or call Docuconf.Contract.Values.to_map/1.
Inspecting it redacts secrets. Durations are integers in duration_unit:
(:millisecond by default, as in the DSL), and a keySet is a
Docuconf.KeySet. Every list and duration encoding is parsed.
It covers the whole contract. File inputs are read under
DOCUCONF_FILE_ROOT: config files in json, yaml and toml (read by
Docuconf.YAML and Docuconf.TOML unless decoders: gives your own, such
as %{"yaml" => &YamlElixir.read_from_string/1}), TLS key pairs, CA
bundles, PKCS#12 keystores, text and binary files. profiles and
overlays are layered as a host that reads config files would: the
variable's default, then the selected profile's default, then an overlay
(an optional file at its path under the file root, read at each
variable's configKey), then the environment. It reads each file once and
starts no watcher, so a contract that declares reload: "watch" on a file
input or an overlay is rejected at load, naming the input:
file motd: reload "watch" is not supported in contract-first mode.
test/conformance_test.exs runs the shared conformance suite (SPEC §12,
conformance/cases.json in docuconf-go) through contract-first mode, one
ExUnit test per case, named by the case id:
DOCUCONF_CONFORMANCE=../docuconf-go/conformance/cases.json \
DOCUCONF_REQUIRE_CONFORMANCE=1 mix test test/conformance_test.exs
Without DOCUCONF_CONFORMANCE the runner reads
../docuconf-go/conformance/cases.json, and skips when it is missing unless
DOCUCONF_REQUIRE_CONFORMANCE=1. CI sets both, using its docuconf-go
checkout. Each case gets a fresh, empty directory as DOCUCONF_FILE_ROOT,
with the case's files written under it.
No capability tags are skipped, and the runner fails if any case is. It
keeps an allow-list of the tags it supports: int64 (Elixir integers hold
every 64-bit value), json-schema (json values are validated against
their JSON Schema), key-set, deprecated, strict-parsing, files,
profiles and overlays. A case with any other tag, including one added
to the suite after this release, is skipped rather than run (SPEC §12), and
then the "skips nothing" test fails, so a new tag is noticed.
test/conformance_export_test.exs declares the shared export fixture
(conformance/export/fixture.yaml) in test/support/fixture_env.ex,
exports it, and compares it with conformance/export/golden.cue using
docuconf conformance export --golden from the same docuconf-go checkout
(DOCUCONF_CLI, else docuconf on PATH). scripts/conformance.sh runs
both:
DOCUCONF_GO_DIR=../docuconf-go scripts/conformance.sh
use Docuconf declaration (SPEC §4.4,
§4.7); see Config-file overlays. Contract-first
mode reads both.deprecated.replaced_by fallback reads.mix test
test/readme_test.exs compiles every Elixir block in this README, and runs
the quickstart, its test and the Phoenix recipe in a fresh VM. The export
test vets the generated contract with cue vet -c against the meta-schema
from docuconf-go (spec/cue).
Point DOCUCONF_SPEC_CUE at that directory (a sibling checkout is found
automatically). The test is skipped when cue is missing, unless
DOCUCONF_REQUIRE_VET=1. Install cue with
go install cuelang.org/go/cmd/[email protected]. Regenerate the golden file with
UPDATE_GOLDEN=1 mix test.
MIT. See LICENSE.
max_keyskey_min_lengthkey_max_length:json | json | decoded JSON | schema, max_length |
String.to_charlist/1"日本""ZÜ01"item_max_length: 4max_length:url:jsonitem_min_lengthitem_max_length{:list, :string}itemMinLengthitemMaxLengthout_of_range:string and unset for every
other type. Values are never trimmed.true or false in any case (TRUE, False), never 1,
yes or on; an integer is decimal digits with an optional sign (007
is 7, never octal), so 0x10, 1_000 and 1e3 are invalid_type; a
float has a digit on each side of any point and an optional exponent, so
.5, 5., inf, NaN and 1e400 are invalid_type; each duration
encoding takes only its own grammar (PT1,5S is ISO 8601, 5 is not a
Go duration); and nothing is trimmed, csv items included (a, b is a
and b).[per_minute: [type: :pos_integer, required: true]]),
from which docuconf generates the schema. A value checked against a
keyword spec is bound to it, so its keys become the spec's atoms.