# ol.sfv

RFC 9651 Structured Field Values for HTTP.

See the project website for info on motivation and design:
&lt;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 <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) |

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