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