diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f32df3f..6d2f3b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index f671504..3876bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 105d8aa..5a3e0d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/GUIDE.md b/GUIDE.md index 3e3603a..c8cfbc6 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -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. @@ -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 @@ -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. --- diff --git a/PLAN.md b/PLAN.md index c872c43..f28b159 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/README.md b/README.md index 63d1211..ba6c7c7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cspell.json b/cspell.json index 3f4c567..a41b4e2 100644 --- a/cspell.json +++ b/cspell.json @@ -36,6 +36,7 @@ "mediaclk", "metatable", "nettype", + "nmos", "ntype", "perr", "popen", diff --git a/parse_sdp-1.4.0-1.rockspec b/parse_sdp-1.5.0-1.rockspec similarity index 98% rename from parse_sdp-1.4.0-1.rockspec rename to parse_sdp-1.5.0-1.rockspec index fbabcc8..bfe8230 100644 --- a/parse_sdp-1.4.0-1.rockspec +++ b/parse_sdp-1.5.0-1.rockspec @@ -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 = { diff --git a/parse_sdp/init.lua b/parse_sdp/init.lua index abe3a46..63099ce 100644 --- a/parse_sdp/init.lua +++ b/parse_sdp/init.lua @@ -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 diff --git a/spec/library_spec.lua b/spec/library_spec.lua index 26fa572..1aaf1eb 100644 --- a/spec/library_spec.lua +++ b/spec/library_spec.lua @@ -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() @@ -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()