Liking cljdoc? Tell your friends :D

Authoring URPX entities in Clojure: a worked tutorial

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])

Example 1: a Season

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])

Example 2: a MetricInput with a nested MetricSpecification

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"}))

Example 3: a TOU structure with local-symbol refs

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.

Example 4: a small but complete rate plan

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.

Example 5: typed values, a node from another document, and an @graph

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.

Where to look next

  • urpx.dsl ns docstring — concise reference for dsl-scope, make-builder, the kwarg-alias rule, and the validation contract.
  • Authoring URPX entities — the API surface in summary.
  • urpx.dsl-test — runnable examples covering every DSL feature, including the full CPAU E-2 round-trip referenced above.
  • Upstream URPX Tutorial — for the canonical URPX shapes these examples mirror.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close