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
:as :ex/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"}
;; forRatePlan and hasCoverageClaim are sh:minCount 1 at v0.5.1.
:for-rate-plan :ex/plan
:has-coverage-claim (dsl/coverage-claim
:completeness-result {:jsonld/id "urpx:complete"})
: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.
The version is nested in its plan and also points back with :for-rate-plan. URPX v0.5.1 permits the snapshot at the top level instead, linked only by :for-rate-plan, with the plan and its snapshot carried together in dsl/urpx-package. Both load through urpx.core/load-rate-plan.
A modifier's alternative dimension often lists typed values and points at a metric input its base document defines. :outside names that node by keyword, with its @type, so the reference is checked against the slot's range like a local one; dsl/literal writes each typed value; and urpx.emit/write-graph writes the result as an @graph document.
(require '[urpx.emit :as emit])
(def vintage
(dsl/dsl-scope
{:outside {:ex/mi-departure "urpx:MetricInput"}} ; defined in the base document
(dsl/alternative-dimension
:as :ex/dim-vintage
:dimension-display-name "Departure vintage"
:custom-dimension-property "departureVintage"
:has-possible-values (dsl/rdf-list [(dsl/literal "2009" "xsd:gYear")
(dsl/literal "2010" "xsd:gYear")])
:references-metric-input [:ex/mi-departure])))
(emit/write-graph [vintage]
{:context {"ex" "https://example.org/"
"urpx" "https://urpx.org/ns/ontology/"
"xsd" "http://www.w3.org/2001/XMLSchema#"}
:json {:indent true :escape-slash false}})
writes, with every object's keys in the emitter's one order:
{
"@context": {
"ex": "https://example.org/",
"urpx": "https://urpx.org/ns/ontology/",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"@graph": [
{
"@id": "ex:dim-vintage",
"@type": "urpx:AlternativeDimension",
"urpx:customDimensionProperty": "departureVintage",
"urpx:dimensionDisplayName": "Departure vintage",
"urpx:hasPossibleValues": [
{
"@list": [
{
"@type": "xsd:gYear",
"@value": "2009"
},
{
"@type": "xsd:gYear",
"@value": "2010"
}
]
}
],
"urpx:referencesMetricInput": [
{
"@id": "ex:mi-departure"
}
]
}
]
}
Naming :ex/mi-departure in a slot that takes another type refuses when the scope closes, under :mistyped.
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 |