Add Configuration
The shop refuses orders above a limit, and the limit is not the same everywhere: large in production, small where the tests run. A value like that is configuration: the component declares it, reads it from the operation context, and the Context sets it.
The component is store.orders, which Create a Component writes; its create forwards to the inherited transit, and Expose It over HTTP serves it at POST /store/orders/.
§What is configuration
Configuration is a parameter of an algorithm that may have different values in different environments.
A value that cannot differ between environments is a constant, and stays in the code, however much
it looks like a setting. Which channels an order confirmation goes over depends on the templates
that exist: the email one is written, the SMS one is not. An environment that named sms in the
configuration would have nothing to send it.
A value is configuration when the answer to both questions is yes:
- Could it differ from one environment to another?
- Would every value the schema allows work with the code as it is?
The limit is one. create refuses an order above it, be it 100 or 5000, and the local
environment sets it lower, so that a test reaches it with a small order.
§Declare the value
# components/store.orders/manifest.toa.yaml
configuration:
schema:
type: object
properties:
limit:
type: integer
minimum: 0
required: [limit]
defaults:
limit: 5000 schema is a JSON Schema of the component’s configuration; defaults is what the component runs
with unless the Context says otherwise.
The schema is there to keep a wrong value from becoming a runtime exception, so make it as tight
as the code. A list of currencies is not an array of any strings: its items are an enum of the
currencies the code can price, tax and charge.
§Read it
The rule needs code, so create stops forwarding and becomes an operation of its own:
// components/store.orders/operations/create.ts
import type { Context, CreateInput, State } from '../types/index.d.ts'
export async function transition (input: CreateInput, entry: State, context: Context) {
if (input.total > context.configuration.limit)
return new Error('LIMIT_EXCEEDED')
Object.assign(entry, input)
return entry
} # components/store.orders/manifest.toa.yaml
operations:
create:
query: false
errors: [LIMIT_EXCEEDED]
input:
type: object
properties:
customer: { type: string }
items: { type: array }
total: { type: integer, minimum: 0 }
required: [customer, items, total] context, the last parameter of every operation, is the operation context. context.configuration is the configuration object, already checked against the schema, so the
operation does not validate it. Never read an environment variable or a file for a setting.
Try it:
$ npm run restart $ curl -i http://localhost:8000/store/orders/
-H 'content-type: application/json'
-d '{"customer":"c1","items":[],"total":7000}' HTTP/1.1 422 Unprocessable Entity
content-type: application/json
{"code":"LIMIT_EXCEEDED"} A component that declares configuration gets it from a service of sys, the process that runs
beside the application.
§Set the value in the Context
# context.toa.yaml
configuration:
identity.tokens:
# …
store.orders:
limit: 100000 context.toa.yaml is the Context: what is true of the application as a whole. toa create wrote it with the addresses of the local infrastructure and one setting, the key the gateway
signs users in with. Values are given per component, by <namespace>.<name>. A value here stands over the manifest’s
default; a default the Context does not mention stays.
The Context is read when .env is written, which npm run restart does first:
$ npm run restart The same request now answers 201 Created.
A key that names no component, or a component that declares no configuration, stops toa env:
Component 'foo.bar' does not request configuration or does not exist. §One value per environment
Append @<environment> to a key to give it a value for one environment only:
# context.toa.yaml
configuration:
store.orders:
limit: 100000
limit@local: 10000 local is the environment of your machine, the one toa env writes when no other is named. The
limit is 10000 there and 100000 in every other environment. An environment is not declared
anywhere; it is named on the command line:
$ npx toa env # local
$ npx toa env staging --as .env.staging # another one, into another file
$ npx toa deploy production The suffix works on any key of the Context, at any depth, which is how the addresses toa create wrote get their production values:
# context.toa.yaml
mongodb: mongodb://localhost
mongodb@production: mongodb://mongo.internal.example.com
exposition:
authorities:
local: localhost:8000
local@production: api.store.example The tests run in local, so they can rely on its limit:
# features/orders.feature
Scenario: An order above the limit
When the request is sent:
"""
POST /store/orders/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
customer: c1
items: []
total: 10001
"""
Then the response is received:
"""
422 Unprocessable Entity
code: LIMIT_EXCEEDED
""" $ npm run env
$ npm run features The steps of this scenario are the ones toa create wrote; Write a Test explains
them.
Everything about environments is in Give Each Environment Its Values.
§Secrets, and changing a value while running
A value that must not be stored with the sources, an API key for instance, is declared with format: secret; the Context then holds only a $NAME reference, as it does for the key under identity.tokens, and the value is supplied to each environment separately. Keep Settings and Secrets out of the Code covers secrets,
nested values and everything else the manifest and the Context take. A value can also be changed
in a running application, without a deployment: Change a Setting without Redeploying.