Service Catalog
Define a typed service catalog once and emit multiple downstream artifacts from it: service summary data, a CI matrix, public endpoint metadata, Docker Compose, and deployment metadata.
Source
examples/service-catalog/types.rank
pub Team = `web-platform` | `payments` | `operations`
pub Tier = `frontend` | `api` | `worker`
pub Exposure = `public` | `internal`
pub ServiceDef = Object {
name: string,
image: string,
owner: Team,
tier: Tier,
exposure: Exposure,
buildCommand: string,
testCommand: string,
port?: number,
dependsOn?: [string],
healthPath?: string,
}
pub ComposeService = Object {
image: string,
ports?: [string],
depends_on?: [string],
environment: Object { ...: string },
}
pub Compose = Object {
version: string,
services: Object { ...: ComposeService },
}
pub ServiceSummary = Object {
name: string,
owner: Team,
tier: Tier,
exposure: Exposure,
}
pub MatrixEntry = Object {
service: string,
owner: Team,
buildCommand: string,
testCommand: string,
}
pub Matrix = Object {
include: [MatrixEntry],
}
pub PublicEndpoint = Object {
service: string,
url: string,
healthPath?: string,
}
pub DeploymentRecord = Object {
service: string,
owner: Team,
image: string,
tier: Tier,
dependsOn: [string],
}
pub DeploymentBundle = Object {
environment: string,
services: [DeploymentRecord],
}
examples/service-catalog/main.rank
/// Emits multiple derived artifacts from one typed service catalog.
/// Shows shared source-of-truth data, collection transforms, and
/// Emit::manifest across JSON and YAML outputs.
use std::Emit
use std::Path
use std::collections::{ filter, map }
use root::types::{
Compose,
ComposeService,
DeploymentBundle,
DeploymentRecord,
Matrix,
MatrixEntry,
PublicEndpoint,
ServiceDef,
ServiceSummary,
}
services: [ServiceDef] = [
{
name: `web`,
image: `ghcr.io/acme/web:2.3.0`,
owner: `web-platform`,
tier: `frontend`,
exposure: `public`,
buildCommand: `npm run build -w web`,
testCommand: `npm test -w web`,
port: 3000,
dependsOn: [`api`],
healthPath: `/healthz`,
},
{
name: `api`,
image: `ghcr.io/acme/api:2.3.0`,
owner: `payments`,
tier: `api`,
exposure: `internal`,
buildCommand: `npm run build -w api`,
testCommand: `npm test -w api`,
port: 8080,
dependsOn: [`redis`],
healthPath: `/health`,
},
{
name: `worker`,
image: `ghcr.io/acme/worker:2.3.0`,
owner: `operations`,
tier: `worker`,
exposure: `internal`,
buildCommand: `npm run build -w worker`,
testCommand: `npm test -w worker`,
dependsOn: [`api`, `redis`],
},
]
web = services[0]
api = services[1]
worker = services[2]
summary_for = |service: ServiceDef| -> ServiceSummary {
return {
name: service.name,
owner: service.owner,
tier: service.tier,
exposure: service.exposure,
}
}
matrix_entry_for = |service: ServiceDef| -> MatrixEntry {
return {
service: service.name,
owner: service.owner,
buildCommand: service.buildCommand,
testCommand: service.testCommand,
}
}
deployment_for = |service: ServiceDef| -> DeploymentRecord {
return {
service: service.name,
owner: service.owner,
image: service.image,
tier: service.tier,
dependsOn: service.dependsOn ?? [],
}
}
compose_service_for = |service: ServiceDef| -> ComposeService {
port = service.port ?? 0
return {
image: service.image,
ports: service.port == null ? [] : [`${port}:${port}`],
depends_on: service.dependsOn ?? [],
environment: {
SERVICE_NAME: service.name,
SERVICE_TIER: service.tier,
OWNER_TEAM: service.owner,
},
}
}
public_endpoint_for = |service: ServiceDef| -> PublicEndpoint {
return {
service: service.name,
url: `https://${service.name}.example.internal`,
healthPath: service.healthPath ?? `/health`,
}
}
summary = services |> map(|service: ServiceDef| summary_for(service))
matrix: Matrix = {
include: services |> map(|service: ServiceDef| matrix_entry_for(service)),
}
public_endpoints = services
|> filter(|service: ServiceDef| service.exposure == `public`)
|> map(|service: ServiceDef| public_endpoint_for(service))
compose: Compose = {
version: `3.9`,
services: {
web: compose_service_for(web),
api: compose_service_for(api),
worker: compose_service_for(worker),
redis: {
image: `redis:7`,
ports: [`6379:6379`],
environment: {
SERVICE_NAME: `redis`,
SERVICE_TIER: `cache`,
OWNER_TEAM: `operations`,
},
},
},
}
deployment_bundle: DeploymentBundle = {
environment: `staging`,
services: services |> map(|service: ServiceDef| deployment_for(service)),
}
pub main = || Emit::manifest({
entries: [
{
path: Path::join([`catalog`, `service-summary.json`]),
format: `json`,
value: summary,
},
{
path: Path::join([`catalog`, `ci-matrix.json`]),
format: `json`,
value: matrix,
},
{
path: Path::join([`catalog`, `public-endpoints.json`]),
format: `json`,
value: public_endpoints,
},
{
path: Path::join([`compose`, `docker-compose.yml`]),
format: `yaml`,
value: compose,
},
{
path: Path::join([`deploy`, `staging-services.yaml`]),
format: `yaml`,
value: deployment_bundle,
},
]
})
Output
Running the example with --file-root writes:
catalog/service-summary.jsoncatalog/ci-matrix.jsoncatalog/public-endpoints.jsoncompose/docker-compose.ymldeploy/staging-services.yaml
{
"include": [
{
"service": "web",
"owner": "web-platform",
"buildCommand": "npm run build -w web",
"testCommand": "npm test -w web"
},
{
"service": "api",
"owner": "payments",
"buildCommand": "npm run build -w api",
"testCommand": "npm test -w api"
},
{
"service": "worker",
"owner": "operations",
"buildCommand": "npm run build -w worker",
"testCommand": "npm test -w worker"
}
]
}
Key concepts
- Single typed catalog keeps ownership, deployment, and CI metadata in one source of truth.
- Collection transforms derive summaries, CI inputs, and public endpoint data with
mapandfilterinstead of repeating hand-maintained files. - Mixed artifact emission uses one Rank program to generate both JSON and YAML outputs.
- Compose and deployment views are projections of the same catalog, not separate config trees that can drift.
Run it
rank examples/service-catalog --file-root out/service-catalog