feat(gb32960): introduce parser profile registry and rule-based vendor selector
Adds a per-context routing layer for InfoBlock parsers, so different peers can be routed to different parser sets (e.g. standard GB/T 32960 vs. a vendor extension that adds blocks in the 0x30~0x7F reserved range). New types in protocol-gb32960: - Gb32960ParserContext (record): version + vin + platformAccount tuple - codec/profile/VendorExtensionCatalog: name -> list of vendor parsers - codec/profile/Gb32960ProfileRegistry: name -> InfoBlockParserRegistry, with a default registry built from the standard parsers and one extra registry per enabled vendor extension (default + vendor parsers) - codec/profile/VendorExtensionSelector: interface returning the matching extension name (or null) for a given context - codec/profile/RuleBasedVendorExtensionSelector: top-down first-match scan over Gb32960Properties.vendorExtensions with a (account,vin) cache keyed on case-insensitive normalized form Gb32960Properties gains a `vendorExtensions` list with per-entry `name` + `match` (platformAccounts / vins / vinPrefixes). All matching fields are OR'd within an entry; entries are scanned top-down with first-match-wins as documented. Gb32960BodyParser keeps its legacy single-registry constructor for back-compat (used by existing tests) and adds a new constructor that takes the profile registry + selector. The parse loop now lives under parse(Gb32960ParserContext, ByteBuffer); the legacy parse(version,body) delegates to it with a minimal context (selector returns null -> default). This commit only introduces the framework. Vendor parsers, autoconfig wiring, and decoder/handler context plumbing land in follow-ups. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package com.lingniu.ingest.protocol.gb32960.codec.profile;
|
||||
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960ParserContext;
|
||||
import com.lingniu.ingest.protocol.gb32960.config.Gb32960Properties;
|
||||
import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class RuleBasedVendorExtensionSelectorTest {
|
||||
|
||||
@Test
|
||||
void platformAccountMatchHits() {
|
||||
var ext = entry("guangdong-fc", List.of("lingniu"), List.of(), List.of());
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc"));
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "LNV1234567890ABCD", "lingniu");
|
||||
assertThat(selector.select(ctx)).isEqualTo("guangdong-fc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void vinExactMatchHits() {
|
||||
var ext = entry("guangdong-fc", List.of(), List.of("LNV1234567890ABCD"), List.of());
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc"));
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "lnv1234567890abcd", null);
|
||||
assertThat(selector.select(ctx))
|
||||
.as("VIN match should be case-insensitive")
|
||||
.isEqualTo("guangdong-fc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void vinPrefixMatchHits() {
|
||||
var ext = entry("guangdong-fc", List.of(), List.of(), List.of("LNVFC", "LZG"));
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc"));
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "LNVFC0001", null);
|
||||
assertThat(selector.select(ctx)).isEqualTo("guangdong-fc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMatchReturnsNull() {
|
||||
var ext = entry("guangdong-fc", List.of("other"), List.of(), List.of());
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc"));
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "ZZZ", "lingniu");
|
||||
assertThat(selector.select(ctx)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstMatchWinsTopDown() {
|
||||
var first = entry("guangdong-fc", List.of("lingniu"), List.of(), List.of());
|
||||
var second = entry("guangdong-fc", List.of(), List.of(), List.of("LNV"));
|
||||
var selector = new RuleBasedVendorExtensionSelector(
|
||||
List.of(first, second), Set.of("guangdong-fc"));
|
||||
// 即便两条都能命中,应返回第一条匹配 entry 的 name(这里凑巧一样)
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "LNV1", "lingniu");
|
||||
assertThat(selector.select(ctx)).isEqualTo("guangdong-fc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyConfigReturnsNullWithoutScanning() {
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(), Set.of("guangdong-fc"));
|
||||
var ctx = new Gb32960ParserContext(ProtocolVersion.V2016, "ANY", "ANY");
|
||||
assertThat(selector.select(ctx)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownExtensionNameRejectedAtConstruction() {
|
||||
var ext = entry("not-in-catalog", List.of("x"), List.of(), List.of());
|
||||
assertThatThrownBy(() ->
|
||||
new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc")))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("not-in-catalog");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resultIsCachedPerAccountVinTuple() {
|
||||
var ext = entry("guangdong-fc", List.of("lingniu"), List.of(), List.of());
|
||||
var selector = new RuleBasedVendorExtensionSelector(List.of(ext), Set.of("guangdong-fc"));
|
||||
var ctx1 = new Gb32960ParserContext(ProtocolVersion.V2016, "VIN1", "lingniu");
|
||||
var ctx2 = new Gb32960ParserContext(ProtocolVersion.V2016, "VIN1", "lingniu");
|
||||
selector.select(ctx1);
|
||||
selector.select(ctx2);
|
||||
// 同一个 (account, vin) 应只产生一个 cache 条目
|
||||
assertThat(selector.cacheSize()).isEqualTo(1);
|
||||
|
||||
var ctx3 = new Gb32960ParserContext(ProtocolVersion.V2016, "VIN2", "lingniu");
|
||||
selector.select(ctx3);
|
||||
assertThat(selector.cacheSize()).isEqualTo(2);
|
||||
}
|
||||
|
||||
private static Gb32960Properties.VendorExtension entry(String name,
|
||||
List<String> accounts,
|
||||
List<String> vins,
|
||||
List<String> vinPrefixes) {
|
||||
var e = new Gb32960Properties.VendorExtension();
|
||||
e.setName(name);
|
||||
var m = new Gb32960Properties.VendorExtension.Match();
|
||||
m.setPlatformAccounts(accounts);
|
||||
m.setVins(vins);
|
||||
m.setVinPrefixes(vinPrefixes);
|
||||
e.setMatch(m);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user