This page mirrors selected examples from the upstream URPX Tutorial and shows the equivalent Clojure form using urpx.dsl. Each section presents the URPX JSON-LD shape (as authored upstream) followed by the same entity built via the DSL — same logical content, less typing, no @type boilerplate, and types that auto-coerce from raw Clojure inputs.
The point isn't to replace the upstream tutorial — it's to make the DSL feel concrete by anchoring its idioms to canonical URPX shapes a tutorial reader has already seen.
(require '[urpx.dsl :as dsl])
URPX Season entities declare a season name and the months it covers (upstream §Complete TOU Structure). Upstream JSON-LD:
{
"@id": "ex:season-summer",
"@type": "Season",
"seasonName": "Summer",
"month": ["--06", "--07", "--08", "--09"],
"yearlyRecurrence": true
}
Via the DSL — auto-coercion of --MM strings into ints, kebab-case kwargs, @type inferred from the schema:
(dsl/season
:jsonld/id "ex:season-summer"
:season-name "Summer"
:month ["--06" "--07" "--08" "--09"]
:yearly-recurrence true)
;; =>
;; {:jsonld/type "urpx:Season"
;; :jsonld/id "ex:season-summer"
;; :urpx/seasonName "Summer"
;; :urpx/month [6 7 8 9]
;; :urpx/yearlyRecurrence true}
You can pass plain ints just as easily — both shapes route through the schema's :decode/urpx-jsonld decoders:
(dsl/season :season-name "Summer" :month [6 7 8 9])
The upstream tutorial's basic plan declares an energy MetricInput that nests a MetricSpecification describing the commodity, measurement kind, and unit (upstream §Step 2: Rate Plan Identity and Version):
{
"@id": "ex:metric-input-energy",
"@type": "MetricInput",
"name": "Standard Energy Measurement",
"metricInputType": { "@id": "urpx:measuredMetricInput" },
"hasMetricSpecification": {
"@id": "ex:metric-spec-energy-supply",
"@type": "MetricSpecification",
"commodity": {"@id": "urpx:electricity"},
"measurementKind": {"@id": "urpx:energy"},
"unitOfMeasure": {"@id": "urpx:kWh"}
}
}
Via the DSL — nested entities are themselves builder calls, and Refs are plain {:jsonld/id …} maps:
(dsl/metric-input
:jsonld/id "ex:metric-input-energy"
:name "Standard Energy Measurement"
:metric-input-type {:jsonld/id "urpx:measuredMetricInput"}
:has-metric-specification
(dsl/metric-specification
:jsonld/id "ex:metric-spec-energy-supply"
:commodity {:jsonld/id "urpx:electricity"}
:measurement-kind {:jsonld/id "urpx:energy"}
:unit-of-measure {:jsonld/id "urpx:kWh"}))
The upstream tutorial's TOU section nests deeply: BillingModel → TOUSchedule → TOUTier → TOUPeriod → TimeBracket, with the bracket referencing a Season declared elsewhere (upstream §Complete TOU Structure). Upstream JSON-LD repeats @id strings to wire everything together:
{
"@type": "TOUTier",
"touTierNumber": 1,
"touTierStandardName": {"@id": "urpx:peakTier"},
"hasTOUPeriod": {
"@id": "ex:period-peak",
"@type": "TOUPeriod",
"touPeriodNumber": 1,
"hasTimeBracket": {
"@type": "TimeBracket",
"startTime": "14:00:00",
"endTime": "19:00:00",
"appliesInSeason": {"@id": "ex:season-summer"},
"dayType": {"@id": "urpx:weekdayDays"},
"includeHolidays": false
}
}
}
Inside a dsl-scope, :as :ex/summer gives the season the @id "ex:summer", and the keyword :ex/summer anywhere else in the scope is a Ref to it:
(dsl/dsl-scope
(dsl/season :as :ex/summer :season-name "Summer" :month [6 7 8 9])
(dsl/tou-tier
:tou-tier-number 1
:tou-tier-standard-name {:jsonld/id "urpx:peakTier"}
:has-tou-period
[(dsl/tou-period
:tou-period-number 1
:name "Summer Peak"
:has-time-bracket
[(dsl/time-bracket
:start-time "14:00:00"
:end-time "19:00:00"
:applies-in-season :ex/summer ; ← {:jsonld/id "ex:summer"}
:day-type {:jsonld/id "urpx:weekdayDays"}
:include-holidays false)])]))
ISO time strings auto-coerce to LocalTime; :ex/summer resolves to {:jsonld/id "ex:summer"}. A reference may come before its declaration. When the scope exits, every keyword referenced but never declared in it throws, in one ex-info naming each keyword and the builders that used it. (dsl/dsl-scope {:id-fn f} …) names @ids with f instead of dsl/curie.
Pulling it together: a non-TOU two-season plan modeled on the published CPAU E-2 schedule, authored entirely via the DSL (upstream §Creating a Simple Rate Plan walks through the equivalent JSON-LD construction in six steps). Same shape the test suite uses to verify end-to-end round-trip (urpx.dsl-test/cpau-e-2-via-dsl).
(dsl/dsl-scope
(let [summer (dsl/season :as :ex/summer :season-name "Summer" :month [5 6 7 8 9 10])
winter (dsl/season :as :ex/winter :season-name "Winter" :month [11 12 1 2 3 4])
mi-energy (dsl/metric-input :as :ex/mi-energy
:name "kWh"
:metric-input-type {:jsonld/id "urpx:profileMetricInput"}
:has-metric-specification
(dsl/metric-specification
:measurement-kind {:jsonld/id "urpx:energy"}
:unit-of-measure {:jsonld/id "urpx:kWh"}))
l-comm (dsl/ledger :as :ex/ledger-commodity
:name "Commodity"
:ledger-type {:jsonld/id "urpx:supplyLedger"}
:references-metric-input [:ex/mi-energy])
pd-cs (dsl/price-definition :as :ex/pd-commodity-summer
:name "Commodity Summer"
:applies-in-season :ex/summer
:references-ledger :ex/ledger-commodity)
pd-cw (dsl/price-definition :as :ex/pd-commodity-winter
:name "Commodity Winter"
:applies-in-season :ex/winter
:references-ledger :ex/ledger-commodity)]
(dsl/rate-plan
:rate-plan-unique-identifier "TUTORIAL-CPAU-E-2"
:name "Tutorial: non-TOU two-season plan"
:initial-effective-date "2025-07-01" ; ← string auto-coerces to LocalDate
:has-rate-plan-version
(dsl/rate-plan-version
;; changeReason and serviceRole are sh:minCount 1 on the version shape and
;; have been since URPX v0.2.1, so the builder requires them; it validates
;; on construct, so omitting either throws here rather than downstream.
:change-reason {:jsonld/id "urpx:rateCase"}
:service-role {:jsonld/id "urpx:bundledServiceRole"}
:effective-date-time "2025-07-01T00:00:00" ; ← string auto-coerces to LocalDateTime
:price-currency "USD"
:has-plan-elements
(dsl/plan-elements
:has-seasons [summer winter]
:has-metric-input [mi-energy]
:has-billing-model
(dsl/billing-model
:has-ledger [l-comm]
:has-price-definition [pd-cs pd-cw]))
:has-prices
(dsl/prices
:has-price-set
[(dsl/price-set
:has-price [(dsl/price :references-price-definition :ex/pd-commodity-summer
:unit-price 0.15075M ; ← a double refuses: write M or a string
:start-date "2025-07-01")
(dsl/price :references-price-definition :ex/pd-commodity-winter
:unit-price 0.09334M
:start-date "2025-07-01")])])))))
The result is a fully-coerced urpx:RatePlan ready for urpx.price/resolve-prices or urpx.emit/write-rate-plan. The test suite verifies that the round-trip — dsl → emit → re-parse → re-coerce — produces a structurally equal plan.
This example authors the v0.2.x nested shape, which URPX v0.5.1 still permits. To author the v0.5.1 shape instead, put the snapshot at the top level and state the link upward with :for-rate-plan, add :has-coverage-claim, and wrap in dsl/urpx-package rather than dsl/urpx-document. Both load through urpx.core/load-rate-plan without the consumer caring which was used.
urpx.dsl ns docstring — concise reference for dsl-scope, make-builder, the kwarg-alias rule, and the validation contract.urpx.dsl-test — runnable examples covering every DSL feature, including the full CPAU E-2 round-trip referenced above.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |