refactor(multimorphic) adopt shared JsonLdProductParser#43
Merged
Conversation
Strict-subset follow-up to PR #42. MultimorphicProductExtractor deletes its private 140-line JSON-LD walker and routes through the shared PinballWizard.Infrastructure.Scraping.JsonLd.JsonLdProductParser that JJP and BoF already consume. Net: -173 lines / +16 lines in MultimorphicProductExtractor.cs. Behavior preserved: all 27 Multimorphic tests pass without modification, including the simultaneous-flat-and-nested-price case that the shared parser was already designed to cover. Validates the shared parser against a third real consumer in production code. BofProductExtractor docstring loses its parenthetical "(when PR #39 lands)" qualifier now that Multimorphic actually consumes the parser. Pre-push audit: /local-review (0 critical / 1 minor — pre-existing JJP drift, deferred as out-of-scope) plus 7-item mechanical checklist (all pass).
This was referenced May 3, 2026
jkeeley2073
added a commit
that referenced
this pull request
May 3, 2026
Third dedup PR in the series (after #42 JsonLdProductParser and #43 Multimorphic adoption). New shared static helper at PinballWizard.Infrastructure.Scraping.OpenGraph.OpenGraphExtractor exposes GetMetaContent(IHtmlDocument doc, string property) which routes meta[property=] first then meta[name=], returning the trimmed content attribute. JJP, BoF, Multimorphic each delete the byte-identical private GetMetaContent and add a using; net -30/+63 across the three consumers and the helper. Behavior preserved exactly — including the content="" returns the empty string semantics that the consumer fallback chains depend on (the ?? operator only triggers on null; changing empty->null would silently change downstream fallback ordering). 12 new tests pin every shape: spec form, loose form, both-prefer-property, missing meta, present-meta-without-content-attribute, whitespace trimming, empty-content-string-parity (load-bearing), first-match-wins on duplicates, null/empty/whitespace guards. Pre-push self-audit: /local-review (0 critical / 3 minor / 7 categories clean — namespace mild misnomer, doc overpromise, unescaped CSS interpolation; all documented and acceptable for an internal helper) plus 7-item mechanical checklist (all pass).
16 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Strict-subset follow-up to PR #42.
MultimorphicProductExtractordeletes its private 140-line JSON-LD walker and routes through the sharedPinballWizard.Infrastructure.Scraping.JsonLd.JsonLdProductParserthat JJP and BoF already consume.Net: −173 lines / +16 lines in
MultimorphicProductExtractor.cs. The class now mirrors the post-#42BofProductExtractortemplate — sameExtractskeleton, same fallback order (product?.Name ?? og:title ?? h1), sameEditionInfoshape — modulo the per-storefront prefix / slug rule /DiscoveredOntoken.The class-level
<remarks>block that named a "future PR" to extract the shared helper is replaced with a forward reference toJsonLdProductParser.BofProductExtractor's docstring also drops the parenthetical "(when PR #39 lands)" qualifier now that Multimorphic actually consumes the parser.Why it matters: validates the shared parser against a third real consumer in production code. The test suite from PR #42 already pinned every shape (including the simultaneous-flat-and-nested case unique to Multimorphic), but a third consumer is the signal that the abstraction generalises cleanly.
Test Plan
dotnet build→ 0 warnings, 0 errorsdotnet test→ 430 / 430 passingExtract_RealMultimorphicShape_BuildsRecord(end-to-end live-shape capture)Extract_NestedPriceSpecOnly_StillReadsPriceExtract_GraphWrappedJsonLd_AlsoWorksExtract_MalformedJsonLd_FallsThroughCleanlyJsonLdProductParser; pre/post comparison againstgit show HEAD:...confirmsGameRecordconstruction at lines 74-88 is byte-identical.Out of Scope
JjpProductExtractor.ExtractSlugis missing theArgumentNullException.ThrowIfNull(productUrl)guard that BoF and Multimorphic both have, andJjpProductExtractor.NormalizeAvailabilityisprivatewhile BoF / Multimorphic make itpublic(the latter is test-driven, the former is a real defect). Tracked as a separate ~3-line PR to keep this one strictly scoped.Checklist
docs/adr/— N/AREADME.mdand/ordocs/are updated in the same PR — N/A (no behavior change)~/.claude/projects/c--projects-PinballWizard/memory/is now stale, it has been updated or removed in the same PRTODO/FIXME/ commented-out code committed<NoWarn>without a comment explaining why and the removal criterionPre-push self-audit
Step 0 —
/local-review(qualitative)/local-reviewand addressed every 🔴 finding before pushExtractSlugnull-guard;NormalizeAvailabilityvisibility inconsistency). Justification: not introduced by this PR; addressing it would expand a strict-subset adoption PR into a sibling-cleanup PR. Tracked as a separate follow-up (see "Out of Scope").Step 1 — Mechanical checklist
*Optionsproperty has at least one real getter call insrc/— N/A (no new options)catch { }— only scopedcatch (JsonException)(preserved behavior)ISourceScraper? — N/A (no new scraper);SourceAliasContractTestsstill passesgit log -1 --format='%an <%ae>'shows personal noreply, not work email