# SFV A 0-dependency Clojure library for parsing and generating Structured Field Values for HTTP (RFC 9651/8941) ## ol.sfv.impl # ol.sfv.impl ## token ```clojure (token s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L15-L15) --- ## token? ```clojure (token? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L16-L16) --- ## decimal ```clojure (decimal x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L18-L18) --- ## decimal? ```clojure (decimal? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L19-L19) --- ## integer ```clojure (integer n) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L21-L21) --- ## integer? ```clojure (integer? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L22-L22) --- ## string ```clojure (string s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L24-L24) --- ## string? ```clojure (string? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L25-L25) --- ## dstring ```clojure (dstring s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L27-L27) --- ## dstring? ```clojure (dstring? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L28-L28) --- ## bytes ```clojure (bytes b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L30-L30) --- ## bytes? ```clojure (bytes? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L31-L31) --- ## bool ```clojure (bool b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L33-L33) --- ## bool? ```clojure (bool? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L34-L34) --- ## date ```clojure (date secs) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L36-L36) --- ## date? ```clojure (date? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L37-L37) --- ## params ```clojure (params & kvs) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L40-L47) --- ## param-get ```clojure (param-get ps k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L49-L49) --- ## param-keys ```clojure (param-keys ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L50-L50) --- ## item ```clojure (item bare) (item bare ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L53-L55) --- ## item? ```clojure (item? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L56-L56) --- ## item-bare ```clojure (item-bare i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L58-L58) --- ## item-params ```clojure (item-params i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L59-L59) --- ## inner-list ```clojure (inner-list items) (inner-list items ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L61-L63) --- ## inner-list? ```clojure (inner-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L65-L65) --- ## inner-items ```clojure (inner-items il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L66-L66) --- ## inner-params ```clojure (inner-params il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L67-L67) --- ## sf-list ```clojure (sf-list members) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L69-L69) --- ## sf-list? ```clojure (sf-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L71-L71) --- ## list-members ```clojure (list-members l) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L72-L72) --- ## sf-dict ```clojure (sf-dict entries) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L73-L73) --- ## sf-dict? ```clojure (sf-dict? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L75-L75) --- ## dict-keys ```clojure (dict-keys d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L76-L76) --- ## dict-get ```clojure (dict-get d k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L77-L77) --- ## dict->pairs ```clojure (dict->pairs d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L78-L78) --- ## flag ```clojure (flag) (flag ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L80-L82) --- ## flag? ```clojure (flag? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L84-L84) --- ## ascii-string ```clojure (ascii-string s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L89-L98) --- ## validate-field-line ```clojure (validate-field-line s) ``` Validate field line characters and return position of first invalid character, or nil if valid [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L100-L109) --- ## init-ctx ```clojure (init-ctx s-or-bytes) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L111-L113) --- ## eof? ```clojure (eof? ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L115-L115) --- ## peek-char ```clojure (peek-char ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L116-L118) --- ## consume-char ```clojure (consume-char ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L120-L121) --- ## skip-ows ```clojure (skip-ows ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L123-L126) --- ## skip-sp ```clojure (skip-sp ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L128-L131) --- ## digit? ```clojure (digit? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L136-L137) --- ## alpha? ```clojure (alpha? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L139-L141) --- ## lcalpha? ```clojure (lcalpha? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L143-L144) --- ## tchar? ```clojure (tchar? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L148-L148) --- ## token-char? ```clojure (token-char? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L149-L149) --- ## lc-hexdig? ```clojure (lc-hexdig? ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L152-L154) --- ## hex-digit-value ```clojure (hex-digit-value ch) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L156-L160) --- ## parse-error ```clojure (parse-error ctx reason & {:keys [found expected]}) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L162-L165) --- ## parse-key ```clojure (parse-key ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L170-L187) --- ## parse-integer-or-decimal ```clojure (parse-integer-or-decimal ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L193-L246) --- ## parse-string ```clojure (parse-string ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L248-L277) --- ## parse-token ```clojure (parse-token ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L278-L300) --- ## parse-byte-sequence ```clojure (parse-byte-sequence ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L301-L332) --- ## parse-sfv-boolean ```clojure (parse-sfv-boolean ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L334-L354) --- ## parse-date ```clojure (parse-date ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L356-L374) --- ## decode-percent-sequence ```clojure (decode-percent-sequence ctx) ``` Decode a percent-encoded sequence (%XX) into a byte value. Returns [byte new-ctx] or throws parse error. [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L376-L389) --- ## parse-display-string ```clojure (parse-display-string ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L391-L498) --- ## parse-bare-item ```clojure (parse-bare-item ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L500-L534) --- ## parse-parameters ```clojure (parse-parameters ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L536-L568) --- ## parse-item ```clojure (parse-item ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L569-L576) --- ## parse-inner-list ```clojure (parse-inner-list ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L578-L602) --- ## parse-item-or-inner-list ```clojure (parse-item-or-inner-list ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L604-L611) --- ## parse-list-members ```clojure (parse-list-members ctx) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L613-L632) --- ## parse-list ```clojure (parse-list s-or-bytes) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L634-L642) --- ## parse-dict ```clojure (parse-dict s-or-bytes) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L644-L693) --- ## parse ```clojure (parse field-type s-or-bytes) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L695-L720) --- ## serialize-integer ```clojure (serialize-integer v) ``` RFC 9651 §4.1.4: Serializing an Integer [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L725-L731) --- ## serialize-decimal ```clojure (serialize-decimal v) ``` RFC 9651 §4.1.5: Serializing a Decimal [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L733-L754) --- ## serialize-string ```clojure (serialize-string v) ``` RFC 9651 §4.1.6: Serializing a String [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L756-L769) --- ## serialize-token ```clojure (serialize-token v) ``` RFC 9651 §4.1.7: Serializing a Token [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L771-L783) --- ## serialize-byte-sequence ```clojure (serialize-byte-sequence v) ``` RFC 9651 §4.1.8: Serializing a Byte Sequence [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L785-L790) --- ## serialize-boolean ```clojure (serialize-boolean v) ``` RFC 9651 §4.1.9: Serializing a Boolean [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L792-L795) --- ## serialize-date ```clojure (serialize-date v) ``` RFC 9651 §4.1.10: Serializing a Date [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L797-L803) --- ## serialize-display-string ```clojure (serialize-display-string v) ``` RFC 9651 §4.1.11: Serializing a Display String [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L805-L820) --- ## serialize-bare-item ```clojure (serialize-bare-item item) ``` RFC 9651 §4.1.3.1: Serializing a Bare Item [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L822-L835) --- ## serialize-key ```clojure (serialize-key k) ``` RFC 9651 §4.1.1.3: Serializing a Key [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L837-L850) --- ## serialize-parameters ```clojure (serialize-parameters params) ``` RFC 9651 §4.1.1.2: Serializing Parameters [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L852-L863) --- ## serialize-item ```clojure (serialize-item item) ``` RFC 9651 §4.1.3: Serializing an Item [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L865-L871) --- ## serialize-inner-list ```clojure (serialize-inner-list inner-list) ``` RFC 9651 §4.1.1.1: Serializing an Inner List [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L873-L888) --- ## serialize-list ```clojure (serialize-list lst) ``` RFC 9651 §4.1.1: Serializing a List [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L890-L906) --- ## serialize-dict ```clojure (serialize-dict dict) ``` RFC 9651 §4.1.2: Serializing a Dictionary [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L908-L934) --- ## serialize ```clojure (serialize x) ``` RFC 9651 §4.1: Serializing Structured Fields [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L936-L943) --- ## combine-field-lines ```clojure (combine-field-lines lines) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L945-L945) ## ol.sfv # ol.sfv RFC 9651 Structured Field Values for HTTP. See the project website for info on motivation and design: <https://github.com/outskirtslabs/sfv> This ns provides parsing and serialization of Structured Fields containing Items, Lists, and Dictionaries with Parameters. Returns precise AST representations that round-trip byte-for-byte with RFC 9651 strings. Primary functions: [`parse`](#parse), [`parse-item`](#parse-item), [`parse-list`](#parse-list), [`parse-dict`](#parse-dict), [`serialize`](#serialize) ## Conventions * `s-or-bytes`: Input string or byte array (decoded as ascii) containing HTTP field value * `field-type`: One of `:item`, `:list`, or `:dict` specifying the top-level structure * `:bare`: The raw value within an Item (Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String) ## Primitive Types Each Item’s `:bare` is one of these RFC 9651 types: | | | | | | --- | --- | --- | --- | | SFV type | Header | AST example | Clojure type (`:value`) | | Integer | `42`, `-17`, `999999999999999` | `{:type :integer :value 1618884473}` | `long` | | Decimal | `3.14`, `-0.5` | `{:type :decimal :value 3.14M}` | `BigDecimal` | | String | `" hello world "` | `{:type :string :value " hello "}` | `java.lang.String` | | Token | `simple-token` | `{:type :token :value " simple-token "}` | `String` | | Byte Sequence | `:SGVsbG8=:` | `{:type :bytes :value }` | `byte[]` | | Boolean | `?1` / `?0` | `{:type :boolean :value true}` | `true` / `false` | | Date | `@1659578233` | `{:type :date :value 1659578233}` | epoch seconds as `long` | | Display String | `%" Gr%c3%bc%c3%9fe "` | `{:type :display :value " Grüße "}` | `String` (percent-decoded, validated) | ## parse ```clojure (parse field-type s-or-bytes) ``` Parse a Structured Field of the given `field-type`. Takes a `field-type` (`:list`, `:dict`, or `:item`) and a string or byte array. Returns an AST representation following RFC 9651. ```clojure (parse :item "42") ;; => {:type :item :bare {:type :integer :value 42} :params []} (parse :dict "max-age=3600, must-revalidate") ;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L39-L53) --- ## parse-list ```clojure (parse-list s-or-bytes) ``` Parse a Structured Field List from `s-or-bytes`. Returns a List AST with `:type :list` and `:members` vector containing Items and Inner Lists. ```clojure (parse-list "apple, pear;sweet=true, orange") ;; => {:type :list :members [...]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L55-L65) --- ## parse-dict ```clojure (parse-dict s-or-bytes) ``` Parse a Structured Field Dictionary from `s-or-bytes`. Returns a Dictionary AST with `:type :dict` and `:entries` as ordered key-value pairs. Each entry maps from a key to either an Item or Inner List. ```clojure (parse-dict "max-age=3600, must-revalidate") ;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L67-L78) --- ## parse-item ```clojure (parse-item s-or-bytes) ``` Parse a Structured Field Item from `s-or-bytes`. Returns an Item AST with `:type :item`, `:bare` value, and `:params` Parameters. The bare value can be Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String. ```clojure (parse-item "42") ;; => {:type :item :bare {:type :integer :value 42} :params []} (parse-item "pear;sweet") ;; => {:type :item :bare {:type :token :value "pear"} :params `"sweet" {...}`} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L80-L94) --- ## serialize ```clojure (serialize x) ``` Serialize a Structured Field AST `x` to its string representation. Takes any parsed AST (Item, List, or Dictionary) and returns the RFC 9651 string. See [`serialize-item`](api/ol-sfv-impl.adoc#serialize-item), [`serialize-list`](api/ol-sfv-impl.adoc#serialize-list), and [`serialize-dict`](api/ol-sfv-impl.adoc#serialize-dict) for type-specific variants. ```clojure (serialize {:type :item :bare {:type :integer :value 42} :params []}) ;; => "42" (serialize-list {:type :list :members [...]}) ;; => "apple, pear;sweet=true, orange" (serialize-dict {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}) ;; => "max-age=3600, must-revalidate" ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L96-L113) --- ## token ```clojure (token s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L116-L117) --- ## token? ```clojure (token? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L119-L120) --- ## decimal ```clojure (decimal x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L122-L123) --- ## decimal? ```clojure (decimal? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L124-L125) --- ## integer ```clojure (integer n) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L127-L128) --- ## integer? ```clojure (integer? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L130-L131) --- ## string ```clojure (string s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L133-L134) --- ## string? ```clojure (string? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L136-L137) --- ## dstring ```clojure (dstring s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L139-L140) --- ## dstring? ```clojure (dstring? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L142-L143) --- ## bytes ```clojure (bytes b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L145-L146) --- ## bytes? ```clojure (bytes? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L148-L149) --- ## bool ```clojure (bool b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L151-L152) --- ## bool? ```clojure (bool? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L154-L155) --- ## date ```clojure (date seconds) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L157-L158) --- ## date? ```clojure (date? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L160-L161) --- ## params ```clojure (params & kvs) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L164-L165) --- ## param-get ```clojure (param-get ps k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L167-L168) --- ## param-keys ```clojure (param-keys ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L170-L171) --- ## item ```clojure (item bare) (item bare ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L174-L178) --- ## item? ```clojure (item? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L180-L181) --- ## item-bare ```clojure (item-bare i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L183-L184) --- ## item-params ```clojure (item-params i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L186-L187) --- ## inner-list ```clojure (inner-list items) (inner-list items ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L190-L194) --- ## inner-list? ```clojure (inner-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L196-L197) --- ## inner-items ```clojure (inner-items il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L199-L200) --- ## inner-params ```clojure (inner-params il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L202-L203) --- ## sf-list ```clojure (sf-list members) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L206-L207) --- ## sf-list? ```clojure (sf-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L209-L210) --- ## list-members ```clojure (list-members l) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L212-L213) --- ## sf-dict ```clojure (sf-dict entries) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L216-L217) --- ## sf-dict? ```clojure (sf-dict? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L219-L220) --- ## dict-keys ```clojure (dict-keys d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L222-L223) --- ## dict-get ```clojure (dict-get d k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L225-L226) --- ## dict->pairs ```clojure (dict->pairs d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L228-L229) --- ## flag ```clojure (flag) (flag ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L232-L236) --- ## flag? ```clojure (flag? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L238-L239) ## Changelog # Changelog All notable changes to this project will be documented in this file. This project uses https://www.taoensso.com/break-versioning[*Break Versioning*]. ## ++[++UNRELEASED++]++ * Removed references in docstrings to old functions that existed in an unreleased version * Reduced stdout during test suite execution ## `v0.1.0` (2025-09-08) This is the first public release of this codebase under the `ol.sfv` name. Private variations of this code has been used in production for a awhile, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning. Please report any problems and let me know if anything is unclear, inconvenient, etc. Thank you! 🙏 ## ol.sfv # ol.sfv > A 0-dependency Clojure library for parsing and generating Structured > Field Values for HTTP (RFC 9651/8941) https://github.com/outskirtslabs/sfv/actions[image:https://github.com/outskirtslabs/sfv/actions/workflows/ci.yml/badge.svg[Build Status]] ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: stable](https://img.shields.io/badge/status-stable-brightgreen.svg) https://clojars.org/com.outskirtslabs/sfv[image:https://img.shields.io/clojars/v/com.outskirtslabs/sfv.svg[Clojars Project]] Structured Field Values (SFV) as defined in [RFC 9651](https://datatracker.ietf.org/doc/html/rfc9651) provide a standardized way to encode complex data structures in HTTP headers. `ol.sfv` library implements the specification, providing parsing and serialization capabilities. This is a low-level library that emits and consumes the RFC 9651 AST, which unfortunately does not map cleanly to Clojure datastructures. For practical use, it should be wrapped in higher-level functions or libraries that implement specific HTTP headers like `Permissions-Policy`, `Signature-Input`, or `Signature` headers (any any future HTTP headers). Key features: * Complete RFC 9651 implementation with AST-level parsing and serialization with zero deps * Extensively tested (`2853 tests, 3220 assertions`) * Precise round-trip fidelity - parse and serialize back to identical strings * Validation and error reporting * JVM/Graal target Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**. ## Installation ```clojure {:deps {com.outskirtslabs/sfv {:mvn/version "0.1.0"}}} ;; Leiningen [com.outskirtslabs/sfv "0.1.0"] ``` ## Quick Start ```clojure (ns myapp.core (:require [ol.sfv :as sfv])) ;; Integer (sfv/parse-item "42") {:type :item :bare {:type :integer :value 42} :params []} ;; They are round trippable (sfv/serialize-item {:type :item :bare {:type :integer :value 42}}) ;; => "42" ;; Display String (sfv/parse-item "%\"Gr%c3%bc%c3%9fe\"") {:type :item :bare {:type :dstring :value "Grüße"} :params []} (sfv/serialize-item {:type :item :bare {:type :dstring :value "السلام عليكم"} :params []}) "\"%d8%a7%d9%84%d8%b3%d9%84%d8%a7%d9%85 %d8%b9%d9%84%d9%8a%d9%83%d9%85\"" ;; Dates (sfv/parse-item "@1659578233") {:type :item, :bare {:type :date, :value 1659578233}, :params []} ;; Items can have params attached (sfv/parse-item "pear;sweet=?1") {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value true}]]} ;; Parse a list with parameters (sfv/parse-list "apple, pear;sweet=true, orange") {:type :list :members [{:type :item :bare {:type :token :value "apple"} :params []} {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value "true"}]]} {:type :item :bare {:type :token :value "orange"} :params []}]} ;; Dictionaries (sfv/parse-dict "max-age=3600, must-revalidate") {:type :dict :entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}] ["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]} ;; Dictionaries with inner lists and params (sfv/parse-dict "trees=(\"spruce\";type=conifer \"oak\";type=deciduous)") {:type :dict :entries [["trees" {:type :inner-list :items [{:type :item :bare {:type :string :value "spruce"} :params [["type" {:type :token :value "conifer"}]]} {:type :item :bare {:type :string :value "oak"} :params [["type" {:type :token :value "deciduous"}]]}] :params []}]]} (sfv/parse-dict "foods=(\"burger\";sandwich=?1 \"pizza\";sandwich=?0 \"hot dog\";sandwich=?1);comprehensive=?0") {:type :dict :entries [["foods" {:type :inner-list :items [{:type :item :bare {:type :string :value "burger"} :params [["sandwich" {:type :boolean :value true}]]} {:type :item :bare {:type :string :value "pizza"} :params [["sandwich" {:type :boolean :value false}]]} {:type :item :bare {:type :string :value "hot dog"} :params [["sandwich" {:type :boolean :value true}]]}] :params [["comprehensive" {:type :boolean :value false}]]}]]} ;; List with Items and Inner List (sfv/parse-list "circle;color=red, square;filled=?0, (triangle;size=3 rectangle;size=4)") {:type :list :members [{:type :item :bare {:type :token :value "circle"} :params [["color" {:type :token :value "red"}]]} {:type :item :bare {:type :token :value "square"} :params [["filled" {:type :boolean :value false}]]} {:type :inner-list :items [{:type :item :bare {:type :token :value "triangle"} :params [["size" {:type :integer :value 3}]]} {:type :item :bare {:type :token :value "rectangle"} :params [["size" {:type :integer :value 4}]]}] :params []}]} ``` ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.sfv/0.1/) * [API Reference](https://docs.outskirtslabs.com/ol.sfv/0.1/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/sfv/issues) ## Data Types `ol.sfv` is a low-level, AST-oriented implementation of RFC 9651. We don’t try to coerce values into nice Clojure shapes; instead we expose a precise tree that round-trips byte-for-byte. This is important because Structured Fields carry ordering information that ordinary Clojure maps can’t reliably preserve across platforms and sizes. ### Primitive types RFC 9651 defines several primitive types, all supported. Each Item’s `:bare` is one of the following: | | | | | | --- | --- | --- | --- | | SFV type | Header | AST example | Clojure type (`:value`) | | Integer | `42`, `-17`, `999999999999999` | `++{++:type :integer :value 1618884473}` | `long` | | Decimal | `3.14`, `-0.5` | `++{++:type :decimal :value 3.14M}` | `BigDecimal` | | String | `"hello world"` | `++{++:type :string :value "hello"}` | `java.lang.String` | | Token | `simple-token` | `++{++:type :token :value "simple-token"}` | `String` | | Byte Sequence | `:SGVsbG8=:` | `+{++:type :bytes :value ++<++platform bytes++>++}` | `byte++[]+` | | Boolean | `?1` / `?0` | `++{++:type :boolean :value true}` | `true` / `false` | | Date | `@1659578233` | `++{++:type :date :value 1659578233}` | epoch seconds as `long` | | Display String | `%"Gr%c3%bc%c3%9fe"` | `++{++:type :display :value "Grüße"}` | `String` (percent-decoded, validated) | * Decimals obey SFV’s constraints (≤3 fractional digits, length limits) and are parsed to `BigDecimal` to avoid float rounding. * Byte sequences are base64 inside the header; we give you the decoded bytes. ### Container types * **Item** — a **bare value** plus optional **parameters** ```clojure {:type :item :bare :params [ [param-name ] ... ]} ``` * **List** — a sequence of **items** or **inner lists** ```clojure {:type :list :members [ ... ]} ``` * **Dictionary** — an **ordered** sequence of key→member entries ```clojure {:type :dict :entries [ [key ] ... ]} ``` * **Inner List** — a parenthesized list (appears inside List/Dictionary) with its own parameters ```clojure {:type :inner-list :items [ ... ] :params [ [param-name ] ... ]} ``` Keys are parsed as lower-case identifiers per the spec. ### Why not plain Clojure maps? Dictionaries and parameter lists in SFV have a defined member order. That order may or may not be semantically meaningful, that depends on the specific header. To make ordering explicit and stable, we represent dictionaries and parameter lists as vectors of ++[++k v++]++ pairs. That’s portable, preserves order, and lets you implement whatever key semantics you need at a higher level. ### Parameters Parameters attach metadata to an Item or an Inner List: * Syntax: `;key++[++=value++]++` repeating after the base value If `=value` is omitted, the parameter value is the boolean `true`. * In the AST, parameters are always a vector of `+[++name value++]+` pairs in the order seen: ```clojure (sfv/parse-item "pear;sweet=?1") {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :boolean :value true}]]} ``` * Parameter values are bare values (not nested items). You’ll see the same `:type`/`:value` shapes as the table above * Ordering is preserved for round-trip fidelity ### Dictionaries Dictionaries map from a key to either an Item or an Inner List. We keep them as an ordered vector of entries: ```clojure (sfv/parse-dict "max-age=3600, must-revalidate") {:type :dict :entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}] ["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]} ``` ## Building Header-Specific Libraries This library is designed to be wrapped by more specific implementations: ```clojure (ns myapp.cache-control (:require [ol.sfv :as sfv])) (defn parse-cache-control [header-value] (let [parsed (sfv/parse-dict header-value)] (reduce (fn [acc [key item]] (let [value (get-in item [:bare :value])] (assoc acc (keyword key) value))) {} (:entries parsed)))) (parse-cache-control "max-age=3600, must-revalidate") ;; => {:max-age 3600, :must-revalidate true} ``` ## Recommended Reading Structured Field Values provide a robust foundation for modern HTTP header design: * https://datatracker.ietf.org/doc/html/rfc9651[RFC 9651: Structured Field Values for HTTP] - The complete specification * https://datatracker.ietf.org/doc/html/rfc9421[RFC 9421: HTTP Message Signatures] - A major consumer of structured fields ## Security See [here](https://github.com/outskirtslabs/sfv/security) for security advisories or to report a security vulnerability. ## License Copyright © 2025 Casey Link casey@outskirtslabs.com Distributed under the [MIT License](./LICENSE) ## ol.sfv # ol.sfv RFC 9651 Structured Field Values for HTTP. See the project website for info on motivation and design: <https://github.com/outskirtslabs/sfv> This ns provides parsing and serialization of Structured Fields containing Items, Lists, and Dictionaries with Parameters. Returns precise AST representations that round-trip byte-for-byte with RFC 9651 strings. Primary functions: [`parse`](#parse), [`parse-item`](#parse-item), [`parse-list`](#parse-list), [`parse-dict`](#parse-dict), [`serialize`](#serialize) ## Conventions * `s-or-bytes`: Input string or byte array (decoded as ascii) containing HTTP field value * `field-type`: One of `:item`, `:list`, or `:dict` specifying the top-level structure * `:bare`: The raw value within an Item (Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String) ## Primitive Types Each Item’s `:bare` is one of these RFC 9651 types: | | | | | | --- | --- | --- | --- | | SFV type | Header | AST example | Clojure type (`:value`) | | Integer | `42`, `-17`, `999999999999999` | `{:type :integer :value 1618884473}` | `long` | | Decimal | `3.14`, `-0.5` | `{:type :decimal :value 3.14M}` | `BigDecimal` | | String | `" hello world "` | `{:type :string :value " hello "}` | `java.lang.String` | | Token | `simple-token` | `{:type :token :value " simple-token "}` | `String` | | Byte Sequence | `:SGVsbG8=:` | `{:type :bytes :value }` | `byte[]` | | Boolean | `?1` / `?0` | `{:type :boolean :value true}` | `true` / `false` | | Date | `@1659578233` | `{:type :date :value 1659578233}` | epoch seconds as `long` | | Display String | `%" Gr%c3%bc%c3%9fe "` | `{:type :display :value " Grüße "}` | `String` (percent-decoded, validated) | ## parse ```clojure (parse field-type s-or-bytes) ``` Parse a Structured Field of the given `field-type`. Takes a `field-type` (`:list`, `:dict`, or `:item`) and a string or byte array. Returns an AST representation following RFC 9651. ```clojure (parse :item "42") ;; => {:type :item :bare {:type :integer :value 42} :params []} (parse :dict "max-age=3600, must-revalidate") ;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L41-L55) --- ## parse-list ```clojure (parse-list s-or-bytes) ``` Parse a Structured Field List from `s-or-bytes`. Returns a List AST with `:type :list` and `:members` vector containing Items and Inner Lists. ```clojure (parse-list "apple, pear;sweet=true, orange") ;; => {:type :list :members [...]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L57-L67) --- ## parse-dict ```clojure (parse-dict s-or-bytes) ``` Parse a Structured Field Dictionary from `s-or-bytes`. Returns a Dictionary AST with `:type :dict` and `:entries` as ordered key-value pairs. Each entry maps from a key to either an Item or Inner List. ```clojure (parse-dict "max-age=3600, must-revalidate") ;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L69-L80) --- ## parse-item ```clojure (parse-item s-or-bytes) ``` Parse a Structured Field Item from `s-or-bytes`. Returns an Item AST with `:type :item`, `:bare` value, and `:params` Parameters. The bare value can be Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String. ```clojure (parse-item "42") ;; => {:type :item :bare {:type :integer :value 42} :params []} (parse-item "pear;sweet") ;; => {:type :item :bare {:type :token :value "pear"} :params `"sweet" {...}`} ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L82-L96) --- ## serialize ```clojure (serialize x) ``` Serialize a Structured Field AST `x` to its string representation. Takes any parsed AST (Item, List, or Dictionary) and returns the RFC 9651 string. ```clojure (serialize {:type :item :bare {:type :integer :value 42} :params []}) ;; => "42" (serialize-list {:type :list :members [...]}) ;; => "apple, pear;sweet=true, orange" (serialize-dict {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}) ;; => "max-age=3600, must-revalidate" ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L98-L114) --- ## token ```clojure (token s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L117-L118) --- ## token? ```clojure (token? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L120-L121) --- ## decimal ```clojure (decimal x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L123-L124) --- ## decimal? ```clojure (decimal? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L125-L126) --- ## integer ```clojure (integer n) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L128-L129) --- ## integer? ```clojure (integer? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L131-L132) --- ## string ```clojure (string s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L134-L135) --- ## string? ```clojure (string? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L137-L138) --- ## dstring ```clojure (dstring s) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L140-L141) --- ## dstring? ```clojure (dstring? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L143-L144) --- ## bytes ```clojure (bytes b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L146-L147) --- ## bytes? ```clojure (bytes? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L149-L150) --- ## bool ```clojure (bool b) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L152-L153) --- ## bool? ```clojure (bool? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L155-L156) --- ## date ```clojure (date seconds) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L158-L159) --- ## date? ```clojure (date? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L161-L162) --- ## params ```clojure (params & kvs) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L165-L166) --- ## param-get ```clojure (param-get ps k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L168-L169) --- ## param-keys ```clojure (param-keys ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L171-L172) --- ## item ```clojure (item bare) (item bare ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L175-L179) --- ## item? ```clojure (item? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L181-L182) --- ## item-bare ```clojure (item-bare i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L184-L185) --- ## item-params ```clojure (item-params i) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L187-L188) --- ## inner-list ```clojure (inner-list items) (inner-list items ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L191-L195) --- ## inner-list? ```clojure (inner-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L197-L198) --- ## inner-items ```clojure (inner-items il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L200-L201) --- ## inner-params ```clojure (inner-params il) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L203-L204) --- ## sf-list ```clojure (sf-list members) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L207-L208) --- ## sf-list? ```clojure (sf-list? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L210-L211) --- ## list-members ```clojure (list-members l) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L213-L214) --- ## sf-dict ```clojure (sf-dict entries) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L217-L218) --- ## sf-dict? ```clojure (sf-dict? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L220-L221) --- ## dict-keys ```clojure (dict-keys d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L223-L224) --- ## dict-get ```clojure (dict-get d k) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L226-L227) --- ## dict->pairs ```clojure (dict->pairs d) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L229-L230) --- ## flag ```clojure (flag) (flag ps) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L233-L237) --- ## flag? ```clojure (flag? x) ``` [source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L239-L240) ## Changelog # Changelog All notable changes to this project will be documented in this file. This project uses https://www.taoensso.com/break-versioning[*Break Versioning*]. ## ++[++UNRELEASED++]++ * Removed references in docstrings to old functions that existed in an unreleased version * Reduced stdout during test suite execution ## `v0.1.0` (2025-09-08) This is the first public release of this codebase under the `ol.sfv` name. Private variations of this code has been used in production for a awhile, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning. Please report any problems and let me know if anything is unclear, inconvenient, etc. Thank you! 🙏 ## ol.sfv # ol.sfv > A 0-dependency Clojure library for parsing and generating Structured > Field Values for HTTP (RFC 9651/8941) ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: stable](https://img.shields.io/badge/status-stable-brightgreen.svg) ![alt=built with garnix](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fgarnix.io%2Fapi%2Fbadges%2Foutskirtslabs%2Fsfv) https://clojars.org/com.outskirtslabs/sfv[image:https://img.shields.io/clojars/v/com.outskirtslabs/sfv.svg[Clojars Project]] Structured Field Values (SFV) as defined in [RFC 9651](https://datatracker.ietf.org/doc/html/rfc9651) provide a standardized way to encode complex data structures in HTTP headers. `ol.sfv` library implements the specification, providing parsing and serialization capabilities. This is a low-level library that emits and consumes the RFC 9651 AST, which unfortunately does not map cleanly to Clojure datastructures. For practical use, it should be wrapped in higher-level functions or libraries that implement specific HTTP headers like `Permissions-Policy`, `Signature-Input`, or `Signature` headers (any any future HTTP headers). Key features: * Complete RFC 9651 implementation with AST-level parsing and serialization with zero deps * Extensively tested (`2853 tests, 3220 assertions`) * Precise round-trip fidelity - parse and serialize back to identical strings * Validation and error reporting * JVM/Graal target Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**. ## Installation ```clojure ;; deps.edn {:deps {com.outskirtslabs/sfv {:mvn/version "0.1.0"}}} ;; Leiningen [com.outskirtslabs/sfv "0.1.0"] ``` ## Quick Start ```clojure (ns myapp.core (:require [ol.sfv :as sfv])) ;; Integer (sfv/parse-item "42") {:type :item :bare {:type :integer :value 42} :params []} ;; They are round trippable (sfv/serialize {:type :item :bare {:type :integer :value 42}}) ;; => "42" ;; Display String (sfv/parse-item "%\"Gr%c3%bc%c3%9fe\"") {:type :item :bare {:type :dstring :value "Grüße"} :params []} (sfv/serialize {:type :item :bare {:type :dstring :value "السلام عليكم"} :params []}) "\"%d8%a7%d9%84%d8%b3%d9%84%d8%a7%d9%85 %d8%b9%d9%84%d9%8a%d9%83%d9%85\"" ;; Dates (sfv/parse-item "@1659578233") {:type :item, :bare {:type :date, :value 1659578233}, :params []} ;; Items can have params attached (sfv/parse-item "pear;sweet=?1") {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value true}]]} ;; Parse a list with parameters (sfv/parse-list "apple, pear;sweet=true, orange") {:type :list :members [{:type :item :bare {:type :token :value "apple"} :params []} {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value "true"}]]} {:type :item :bare {:type :token :value "orange"} :params []}]} ;; Dictionaries (sfv/parse-dict "max-age=3600, must-revalidate") {:type :dict :entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}] ["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]} ;; Dictionaries with inner lists and params (sfv/parse-dict "trees=(\"spruce\";type=conifer \"oak\";type=deciduous)") {:type :dict :entries [["trees" {:type :inner-list :items [{:type :item :bare {:type :string :value "spruce"} :params [["type" {:type :token :value "conifer"}]]} {:type :item :bare {:type :string :value "oak"} :params [["type" {:type :token :value "deciduous"}]]}] :params []}]]} (sfv/parse-dict "foods=(\"burger\";sandwich=?1 \"pizza\";sandwich=?0 \"hot dog\";sandwich=?1);comprehensive=?0") {:type :dict :entries [["foods" {:type :inner-list :items [{:type :item :bare {:type :string :value "burger"} :params [["sandwich" {:type :boolean :value true}]]} {:type :item :bare {:type :string :value "pizza"} :params [["sandwich" {:type :boolean :value false}]]} {:type :item :bare {:type :string :value "hot dog"} :params [["sandwich" {:type :boolean :value true}]]}] :params [["comprehensive" {:type :boolean :value false}]]}]]} ;; List with Items and Inner List (sfv/parse-list "circle;color=red, square;filled=?0, (triangle;size=3 rectangle;size=4)") {:type :list :members [{:type :item :bare {:type :token :value "circle"} :params [["color" {:type :token :value "red"}]]} {:type :item :bare {:type :token :value "square"} :params [["filled" {:type :boolean :value false}]]} {:type :inner-list :items [{:type :item :bare {:type :token :value "triangle"} :params [["size" {:type :integer :value 3}]]} {:type :item :bare {:type :token :value "rectangle"} :params [["size" {:type :integer :value 4}]]}] :params []}]} ``` ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.sfv/0.1/) * [API Reference](https://docs.outskirtslabs.com/ol.sfv/0.1/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/sfv/issues) ## Data Types `ol.sfv` is a low-level, AST-oriented implementation of RFC 9651. We don’t try to coerce values into nice Clojure shapes; instead we expose a precise tree that round-trips byte-for-byte. This is important because Structured Fields carry ordering information that ordinary Clojure maps can’t reliably preserve across platforms and sizes. ### Primitive types RFC 9651 defines several primitive types, all supported. Each Item’s `:bare` is one of the following: | | | | | | --- | --- | --- | --- | | SFV type | Header | AST example | Clojure type (`:value`) | | Integer | `42`, `-17`, `999999999999999` | `++{++:type :integer :value 1618884473}` | `long` | | Decimal | `3.14`, `-0.5` | `++{++:type :decimal :value 3.14M}` | `BigDecimal` | | String | `"hello world"` | `++{++:type :string :value "hello"}` | `java.lang.String` | | Token | `simple-token` | `++{++:type :token :value "simple-token"}` | `String` | | Byte Sequence | `:SGVsbG8=:` | `+{++:type :bytes :value ++<++platform bytes++>++}` | `byte++[]+` | | Boolean | `?1` / `?0` | `++{++:type :boolean :value true}` | `true` / `false` | | Date | `@1659578233` | `++{++:type :date :value 1659578233}` | epoch seconds as `long` | | Display String | `%"Gr%c3%bc%c3%9fe"` | `++{++:type :display :value "Grüße"}` | `String` (percent-decoded, validated) | * Decimals obey SFV’s constraints (≤3 fractional digits, length limits) and are parsed to `BigDecimal` to avoid float rounding. * Byte sequences are base64 inside the header; we give you the decoded bytes. ### Container types * **Item** — a **bare value** plus optional **parameters** ```clojure {:type :item :bare :params [ [param-name ] ... ]} ``` * **List** — a sequence of **items** or **inner lists** ```clojure {:type :list :members [ ... ]} ``` * **Dictionary** — an **ordered** sequence of key→member entries ```clojure {:type :dict :entries [ [key ] ... ]} ``` * **Inner List** — a parenthesized list (appears inside List/Dictionary) with its own parameters ```clojure {:type :inner-list :items [ ... ] :params [ [param-name ] ... ]} ``` Keys are parsed as lower-case identifiers per the spec. ### Why not plain Clojure maps? Dictionaries and parameter lists in SFV have a defined member order. That order may or may not be semantically meaningful, that depends on the specific header. To make ordering explicit and stable, we represent dictionaries and parameter lists as vectors of ++[++k v++]++ pairs. That’s portable, preserves order, and lets you implement whatever key semantics you need at a higher level. ### Parameters Parameters attach metadata to an Item or an Inner List: * Syntax: `;key++[++=value++]++` repeating after the base value If `=value` is omitted, the parameter value is the boolean `true`. * In the AST, parameters are always a vector of `+[++name value++]+` pairs in the order seen: ```clojure (sfv/parse-item "pear;sweet=?1") {:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :boolean :value true}]]} ``` * Parameter values are bare values (not nested items). You’ll see the same `:type`/`:value` shapes as the table above * Ordering is preserved for round-trip fidelity ### Dictionaries Dictionaries map from a key to either an Item or an Inner List. We keep them as an ordered vector of entries: ```clojure (sfv/parse-dict "max-age=3600, must-revalidate") {:type :dict :entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}] ["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]} ``` ## Building Header-Specific Libraries This library is designed to be wrapped by more specific implementations: ```clojure (ns myapp.cache-control (:require [ol.sfv :as sfv])) (defn parse-cache-control [header-value] (let [parsed (sfv/parse-dict header-value)] (reduce (fn [acc [key item]] (let [value (get-in item [:bare :value])] (assoc acc (keyword key) value))) {} (:entries parsed)))) (parse-cache-control "max-age=3600, must-revalidate") ;; => {:max-age 3600, :must-revalidate true} ``` ## Recommended Reading Structured Field Values provide a robust foundation for modern HTTP header design: * https://datatracker.ietf.org/doc/html/rfc9651[RFC 9651: Structured Field Values for HTTP] - The complete specification * https://datatracker.ietf.org/doc/html/rfc9421[RFC 9421: HTTP Message Signatures] - A major consumer of structured fields ## Security See [here](https://github.com/outskirtslabs/sfv/security) for security advisories or to report a security vulnerability. ## License Copyright © 2025 Casey Link casey@outskirtslabs.com Distributed under the [MIT License](./LICENSE)