Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
luarocks install dkjson
luarocks install argparse
luarocks install busted
- run: luarocks make ./parse_sdp-1.4.0-1.rockspec
- run: luarocks make ./parse_sdp-1.5.0-1.rockspec
- run: busted spec/

conformance:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- **`sdp.params_get(params, key)`** — re-exported at the top level as the
inner companion to `sdp.attr_get` / `sdp.attrs_get`. Looks up a value by
key in an ordered `a=fmtp` / `a=privacy` param list (returns the value,
`true` for a bare flag, or `nil`). Previously reachable only via
`parse_sdp.grammar.base.params_get`; that path still works. Note the
argument shape differs from `attr_get`: `params_get` takes the inner
list (`fmtp.params`), not the block.

## [1.4.0] — 2026-05-31

### Added
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ local doc, err = sdp.parse(text, "st2110") -- parse + validate ST 2110
local doc, err = sdp.parse(text, "ipmx") -- parse + validate IPMX
local doc = sdp.new(table) -- wrap table as doc (no validation)

-- Attribute accessors (module-level; mirror grammar.base.params_get)
-- Attribute / param accessors (module-level)
local a = sdp.attr_get(block, name) -- first decomposed attr by name, or nil
local as = sdp.attrs_get(block, name) -- all matches in order (empty table if none)
local v = sdp.params_get(params, key) -- value/true/nil from an fmtp/privacy params list
-- (takes the inner list, e.g. fmtp.params, not the block)

-- doc methods (via metatable)
doc:validate() -- validate as RFC 8866; true or nil, err
Expand Down
32 changes: 27 additions & 5 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,9 @@ local doc, err = sdp.parse(text, "sdp", { policy = policy })

Look up decomposed attributes by name on a block. A *block* is any table
that carries an `attributes` array — `doc.session` or any `doc.media[i]`
entry. These mirror `parse_sdp.grammar.base.params_get` (the inner-`fmtp`
accessor) for the outer attribute list, so consumers never hand-roll the
scan over the ordered `attributes` array.
entry. These mirror [`sdp.params_get`](#sdpparams_getparams-key) (the
inner-`fmtp` accessor) for the outer attribute list, so consumers never
hand-roll the scan over the ordered `attributes` array.

- `sdp.attr_get(block, name)` returns the **first** attribute table whose
`name` matches, or `nil` when none is present.
Expand Down Expand Up @@ -424,6 +424,28 @@ end
local grp = sdp.attr_get(doc.session, "group") -- session-level lookup
```

#### `sdp.params_get(params, key)`

Look up a value in an ordered param list by key — the inner companion to
`attr_get` / `attrs_get`. `a=fmtp` and `a=privacy` carry their key/value
pairs as an ordered `params` list (input order preserved for byte-faithful
round-trip); `params_get` returns the value for `key`, or `nil` when
absent. A bare flag (a key with no `=value`) has `true` as its value.

**Note the argument shape:** unlike `attr_get`, which takes a *block*,
`params_get` takes the **inner list directly** — `fmtp.params`, not the
`fmtp` attribute table. Param lists have no wrapping object, so there is
nothing block-like to pass. nil-safe: a `nil` list yields `nil`.

```lua
local doc = sdp.parse(text, "st2110")
local fmtp = sdp.attr_get(doc.media[1], "fmtp") -- the fmtp attribute table
if fmtp then
local width = sdp.params_get(fmtp.params, "width") -- "1920", or nil
local seg = sdp.params_get(fmtp.params, "interlace") -- true if bare flag
end
```

---

### Doc methods
Expand Down Expand Up @@ -826,8 +848,8 @@ rather than scanning it yourself.

`fmtp.params` and `privacy.params` are **ordered lists** of `{key, value}`
sub-tables (input order preserved for byte-faithful round-trip). Use
`parse_sdp.grammar.base.params_get(params, key)` to look one up; a bare
flag has `true` as its value.
[`sdp.params_get(params, key)`](#sdpparams_getparams-key) to look one up; a
bare flag has `true` as its value.

---

Expand Down
16 changes: 16 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ but nothing for the outer attribute list, so every consumer hand-rolled a
- [x] **Release slice**: tagged `v1.4.0`, uploaded rockspec, published to
LuaRocks (2026-05-31).

## 1.5.0 — params_get top-level re-export

Follow-up consumer note (the lnmos nmos/sdp.lua layer): 1.4.0 promoted the
outer attribute accessors to the public module but left `params_get` (the
inner `a=fmtp` / `a=privacy` lookup) reachable only via
`parse_sdp.grammar.base`. That left the inner companion as the odd one out,
forcing consumers to cross into a grammar internal mid-task. Re-exporting it
restores one public surface for name lookups.

- [x] `sdp.params_get(params, key)` re-exported on the public module; the
`grammar.base` path still works for existing callers.
- [x] Documented the argument-shape asymmetry vs `attr_get` (takes the
inner list, e.g. `fmtp.params`, not the block) at the re-export, in the
GUIDE API reference, and in a dedicated test.
- [ ] **Release slice**: tag `v1.5.0`, upload rockspec, publish.

## Next pass — GUIDE.md Troubleshooting recipes

A field engineer with an unfamiliar `err.id` wants "what does this
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ print(doc.media[1].port)
-- Look attributes up by name (no hand-rolled scan of the ordered array)
local rtpmap = sdp.attr_get(doc.media[1], "rtpmap") -- first match, or nil
local all = sdp.attrs_get(doc.media[1], "rtpmap") -- every match, in order
local fmtp = sdp.attr_get(doc.media[1], "fmtp")
local width = fmtp and sdp.params_get(fmtp.params, "width") -- inner fmtp param

-- It also has methods
local ok, err = doc:validate("st2110") -- re-validate after mutation
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"mediaclk",
"metatable",
"nettype",
"nmos",
"ntype",
"perr",
"popen",
Expand Down
4 changes: 2 additions & 2 deletions parse_sdp-1.4.0-1.rockspec → parse_sdp-1.5.0-1.rockspec
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package = "parse_sdp"
version = "1.4.0-1"
version = "1.5.0-1"

source = {
url = "git+https://github.com/andrewstarks/parse_sdp.git",
tag = "v1.4.0",
tag = "v1.5.0",
}

description = {
Expand Down
13 changes: 11 additions & 2 deletions parse_sdp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,20 @@ M.default_policy = errors.default_policy
--- Look up decomposed attributes by name on a block (a `doc.session` or
-- `doc.media[i]` table). `attr_get` returns the first matching attribute
-- table (or nil); `attrs_get` returns an array of all matches in document
-- order. Both nil-safe. Mirror `parse_sdp.grammar.base.params_get` (the
-- inner-fmtp accessor) for the outer `attributes` array.
-- order. Both nil-safe. Companion to `params_get` (the inner-fmtp accessor)
-- for the outer `attributes` array.
M.attr_get = grammar_base.attr_get
M.attrs_get = grammar_base.attrs_get

--- Look up a value by key in an ordered param list — the inner companion
-- to attr_get / attrs_get. `a=fmtp` / `a=privacy` carry their key/value
-- pairs as an ordered `params` list; returns the value, `true` for a bare
-- flag, or nil when absent. nil-safe. NOTE the argument shape: unlike
-- attr_get (which takes a block), params_get takes the inner list directly
-- — `fmtp.params`, not the `fmtp` table — since param lists have no
-- wrapping object.
M.params_get = grammar_base.params_get

-- Exposed for spec access; not part of the public contract.
M._errors = errors

Expand Down
55 changes: 55 additions & 0 deletions spec/library_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ local MULTI_ATTR_SDP = table.concat({
"a=recvonly",
}, "\r\n") .. "\r\n"

-- fmtp carrying a bare flag (`interlace`) alongside k=v pairs. Exercises
-- params_get value lookup and the bare-flag → `true` case.
local PARAMS_SDP = table.concat({
"v=0",
"o=- 1234567890 1 IN IP4 192.168.1.1",
"s=Params",
"t=0 0",
"a=ts-refclk:ptp=IEEE1588-2008:00-11-22-FF-FE-33-44-55:0",
"m=video 5000 RTP/AVP 96",
"c=IN IP4 239.100.0.1/64",
"a=rtpmap:96 raw/90000",
"a=fmtp:96 sampling=YCbCr-4:2:2; width=1920; height=1080; exactframerate=25; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2022; TP=2110TPN; interlace",
"a=mediaclk:direct=0",
"a=ts-refclk:ptp=IEEE1588-2008:00-11-22-FF-FE-33-44-55:0",
}, "\r\n") .. "\r\n"

-- ── 1. Module loads ──────────────────────────────────────────────────────────

describe("library: parse_sdp module loads", function()
Expand Down Expand Up @@ -672,6 +688,45 @@ describe("library: sdp.attr_get() / sdp.attrs_get()", function()
end)
end)

describe("library: sdp.params_get()", function()
-- NOT-SPEC: library
it("returns the value for a present key", function()
local doc = sdp.parse(PARAMS_SDP, "st2110")
local fmtp = sdp.attr_get(doc.media[1], "fmtp")
assert.is_table(fmtp)
assert.equal("1920", sdp.params_get(fmtp.params, "width"))
assert.equal("YCbCr-4:2:2", sdp.params_get(fmtp.params, "sampling"))
end)

-- NOT-SPEC: library
it("returns true for a bare flag", function()
local doc = sdp.parse(PARAMS_SDP, "st2110")
local fmtp = sdp.attr_get(doc.media[1], "fmtp")
assert.equal(true, sdp.params_get(fmtp.params, "interlace"))
end)

-- NOT-SPEC: library
it("returns nil for an absent key", function()
local doc = sdp.parse(PARAMS_SDP, "st2110")
local fmtp = sdp.attr_get(doc.media[1], "fmtp")
assert.is_nil(sdp.params_get(fmtp.params, "nonesuch"))
end)

-- NOT-SPEC: library
it("is nil-safe: nil list yields nil", function()
assert.is_nil(sdp.params_get(nil, "width"))
end)

-- NOT-SPEC: library
it("takes the inner params list, not the attribute table", function()
-- Documented asymmetry vs attr_get: params_get scans `fmtp.params`,
-- so passing the fmtp table itself finds nothing.
local doc = sdp.parse(PARAMS_SDP, "st2110")
local fmtp = sdp.attr_get(doc.media[1], "fmtp")
assert.is_nil(sdp.params_get(fmtp, "width"))
end)
end)

describe("library: sdp.parse(text, mode, opts) policy validation", function()
-- NOT-SPEC: library
it("accepts opts.policy with all-known ids", function()
Expand Down
Loading