diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c50050..a0a696d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 0.14.3 (2026-10-20) + +### Fixed + +- Accept protobuf native request and response objects (`request_native` and `adm_native`, including their camelCase spellings) in `proto-json`. Validate native assets and boolean flags, reject conflicting oneof alternatives, and retain required-asset checks in paired validation. +- Warn when `site.domain` contains URL syntax or whitespace. Full page URLs belong in `site.page`. + ## 0.14.2 (2026-10-20) ### Changed diff --git a/Cargo.lock b/Cargo.lock index c85af6c..4d1656f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,7 +1154,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rtblint" -version = "0.14.2" +version = "0.14.3" dependencies = [ "rtblint-core", "rtblint-resolve", @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "rtblint-core" -version = "0.14.2" +version = "0.14.3" dependencies = [ "jsonschema", "serde", @@ -1173,7 +1173,7 @@ dependencies = [ [[package]] name = "rtblint-grpc" -version = "0.14.2" +version = "0.14.3" dependencies = [ "http", "mimalloc", @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "rtblint-mcp" -version = "0.14.2" +version = "0.14.3" dependencies = [ "rtblint-core", "serde_json", @@ -1203,7 +1203,7 @@ dependencies = [ [[package]] name = "rtblint-resolve" -version = "0.14.2" +version = "0.14.3" dependencies = [ "rtblint-core", "serde", @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "rtblint-wasm" -version = "0.14.2" +version = "0.14.3" dependencies = [ "rtblint-core", "serde", diff --git a/Cargo.toml b/Cargo.toml index a25e41e..2d9431f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = ["fuzz"] resolver = "2" [workspace.package] -version = "0.14.2" +version = "0.14.3" edition = "2021" rust-version = "1.74" authors = ["Aleks "] diff --git a/README.md b/README.md index e012273..0fabbd5 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,13 @@ cat request.json | rtblint validate --stdin /app-ads//app-ads.txt ``` +Protobuf JSON also supports the IAB schema's native object alternatives: +`imp.native.request_native` (`requestNative`) and `bid.adm_native` +(`admNative`). Their native flags use booleans. The string alternatives +`request` and `adm` keep their JSON-encoded Native Ads markup. Supply one +alternative per oneof. Native assets remain validated and paired against +required request assets. See the [IAB protobuf schema](https://github.com/IABTechLab/openrtb-proto-v2/blob/master/openrtb-core/src/main/protobuf/openrtb.proto). + Nothing is fetched; populate the cache yourself. `--batch` lints one JSON object per line from a file or stdin. `--summary` adds rule-frequency totals for a captured stream (`--summary bids.ndjson` for the histogram alone). See [ARTF](#artf) for `--type artf-request` and `--type artf-response`. Exit codes: 0 valid, 1 validation errors, 2 usage or I/O error. diff --git a/crates/rtblint-core/src/dialect.rs b/crates/rtblint-core/src/dialect.rs index dde581c..bc89e00 100644 --- a/crates/rtblint-core/src/dialect.rs +++ b/crates/rtblint-core/src/dialect.rs @@ -36,6 +36,16 @@ pub enum Dialect { } impl Dialect { + /// Object alternatives declared by the IAB protobuf native oneofs. + pub(crate) fn native_field(self, object: &str, field: &str) -> bool { + self == Self::ProtoJson + && matches!( + (object, field), + ("Native", "request_native" | "requestNative") + | ("Bid", "adm_native" | "admNative") + ) + } + pub fn as_str(self) -> &'static str { match self { Self::SpecJson => "spec-json", diff --git a/crates/rtblint-core/src/native.rs b/crates/rtblint-core/src/native.rs index 7ebbb31..8b24781 100644 --- a/crates/rtblint-core/src/native.rs +++ b/crates/rtblint-core/src/native.rs @@ -13,6 +13,7 @@ use crate::{Issue, Severity}; const SECTION_REQUEST: &str = "Native Ads 1.2 §4"; const SECTION_RESPONSE: &str = "Native Ads 1.2 §5"; +const SECTION_PROTO: &str = "https://github.com/IABTechLab/openrtb-proto-v2/blob/master/openrtb-core/src/main/protobuf/openrtb.proto"; const VENDOR_RANGE: i64 = 500; const DATA_TYPES: &[i64] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; @@ -22,6 +23,151 @@ const EVENT_METHODS: &[i64] = &[1, 2]; const ASSET_KEYS: [&str; 4] = ["title", "img", "video", "data"]; +/// IAB openrtb-proto-v2 openrtb.proto: Native.request_oneof and Bid.adm_oneof. +pub(crate) fn proto_alternative( + object: &Map, + request: bool, +) -> Option<(&'static str, &Value)> { + let names = if request { + ["request_native", "requestNative"] + } else { + ["adm_native", "admNative"] + }; + names.into_iter().find_map(|name| { + object + .get(name) + .filter(|value| !value.is_null()) + .map(|value| (name, value)) + }) +} + +/// Native protobuf flags are bools. Normalize only those fields for the +/// shared semantic checks and the required-asset index. Extensions stay opaque. +pub(crate) fn normalize_proto_markup(root: &mut Map, request: bool) { + if request { + for field in ["aurlsupport", "durlsupport", "privacy"] { + normalize_proto_flag(root.get_mut(field)); + } + } + if let Some(assets) = root.get_mut("assets").and_then(Value::as_array_mut) { + for asset in assets.iter_mut().filter_map(Value::as_object_mut) { + normalize_proto_flag(asset.get_mut("required")); + } + } +} + +fn normalize_proto_flag(value: Option<&mut Value>) { + if let Some(value) = value { + if let Some(flag) = value.as_bool() { + *value = Value::from(i64::from(flag)); + } + } +} + +pub(crate) fn validate_proto_alternative( + object_name: &str, + object: &Map, + field: &str, + value: &Value, + path: &str, + require_asset_id: bool, + issues: &mut Vec, +) { + if value.is_null() { + return; + } + let request = object_name == "Native"; + let section = if request { + SECTION_REQUEST + } else { + SECTION_RESPONSE + }; + let string_field = if request { "request" } else { "adm" }; + let other_name = match field { + "request_native" => "requestNative", + "requestNative" => "request_native", + "adm_native" => "admNative", + _ => "adm_native", + }; + if [string_field, other_name] + .iter() + .any(|name| object.get(*name).is_some_and(|other| !other.is_null())) + { + issues.push(issue( + "openrtb.dialect.oneof_conflict", + Severity::Error, + format!("The {string_field} protobuf oneof accepts only one alternative and one field spelling."), + path.to_owned(), + SECTION_PROTO, + )); + } + let Some(mut root) = value.as_object().cloned() else { + issues.push(issue( + "openrtb.type.mismatch", + Severity::Error, + format!("{field} must be a protobuf native object."), + path.to_owned(), + section, + )); + return; + }; + if request { + for field in ["aurlsupport", "durlsupport", "privacy"] { + validate_proto_flag(root.get(field), &join_path(path, field), issues); + } + } + if let Some(assets) = root.get("assets").and_then(Value::as_array) { + for (index, asset) in assets.iter().enumerate() { + validate_proto_flag( + asset.get("required"), + &format!("{path}.assets[{index}].required"), + issues, + ); + } + } + normalize_proto_markup(&mut root, request); + if request { + let ver = root + .get("ver") + .and_then(Value::as_str) + .or_else(|| object.get("ver").and_then(Value::as_str)); + validate_markup_request(&root, path, ver, require_asset_id, issues); + } else { + validate_markup_response(&root, path, issues); + if object + .get("mtype") + .and_then(Value::as_i64) + .is_some_and(|mtype| mtype != 4) + { + issues.push(issue( + "openrtb.bid.adm.media_type_mismatch", + Severity::Error, + String::from( + "Protobuf native markup requires native mtype 4 when mtype is supplied.", + ), + path.to_owned(), + section, + )); + } + } +} + +fn validate_proto_flag(value: Option<&Value>, path: &str, issues: &mut Vec) { + if let Some(value) = value.filter(|value| !value.is_null() && !value.is_boolean()) { + issues.push(issue( + if value.is_number() { + "openrtb.dialect.integer_for_bool" + } else { + "openrtb.type.mismatch" + }, + Severity::Error, + String::from("This native protobuf flag must be true or false in proto-json."), + path.to_owned(), + SECTION_PROTO, + )); + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum NativeAssetKind { Title, diff --git a/crates/rtblint-core/src/pair.rs b/crates/rtblint-core/src/pair.rs index fe93ab9..54be63a 100644 --- a/crates/rtblint-core/src/pair.rs +++ b/crates/rtblint-core/src/pair.rs @@ -74,8 +74,15 @@ pub(crate) fn validate_bid_response_against_request( return finalize_result(result.issues); } - let context = RequestContext::index(profile, request); - cross_validate(version, profile, &context, response, &mut result.issues); + let context = RequestContext::index(dialect, profile, request); + cross_validate( + version, + dialect, + profile, + &context, + response, + &mut result.issues, + ); crate::profile::push_profile_pair(profile, request, response, &mut result.issues); finalize_result(result.issues) } @@ -111,7 +118,7 @@ struct ImpContext<'a> { } impl<'a> RequestContext<'a> { - fn index(profile: Profile, request: &'a Map) -> Self { + fn index(dialect: Dialect, profile: Profile, request: &'a Map) -> Self { let mut imps = HashMap::new(); for imp in request .get("imp") @@ -137,7 +144,19 @@ impl<'a> RequestContext<'a> { let native_assets = imp .get("native") - .and_then(|value| crate::profile::compat::native_request(profile, value)) + .and_then(|value| { + if dialect == Dialect::ProtoJson { + if let Some((_, source)) = value + .as_object() + .and_then(|object| native::proto_alternative(object, true)) + { + let mut root = source.as_object()?.clone(); + native::normalize_proto_markup(&mut root, true); + return Some(root); + } + } + crate::profile::compat::native_request(profile, value) + }) .map(|request| native::index_markup_request(&request)) .unwrap_or_default(); @@ -179,6 +198,7 @@ fn string_list(value: Option<&Value>) -> Option> { /// Pass two: cross-checks over the already-validated response. fn cross_validate( version: OpenRtbVersion, + dialect: Dialect, profile: Profile, context: &RequestContext<'_>, response: &Map, @@ -273,6 +293,7 @@ fn cross_validate( }; let bid_path = format!("seatbid[{seatbid_index}].bid[{bid_index}]"); cross_validate_bid( + dialect, profile, context, bid, @@ -285,6 +306,7 @@ fn cross_validate( } fn cross_validate_bid( + dialect: Dialect, profile: Profile, context: &RequestContext<'_>, bid: &Map, @@ -310,6 +332,19 @@ fn cross_validate_bid( return; }; + if dialect == Dialect::ProtoJson && imp.native { + if let Some((field, source)) = native::proto_alternative(bid, false) { + if let Some(response) = source.as_object() { + native::validate_response_against_request( + &imp.native_assets, + response, + &format!("{bid_path}.{field}"), + issues, + ); + } + } + } + if let Some(mtype) = bid.get("mtype").and_then(integer_value) { let (offered, media_label) = match mtype { 1 => (imp.banner, "banner"), @@ -389,6 +424,7 @@ fn cross_validate_bid( .and_then(Value::as_str) .is_some_and(|markup| !markup.trim().is_empty()) && crate::profile::compat::adm_native(profile) + && !(dialect == Dialect::ProtoJson && native::proto_alternative(bid, false).is_some()) { if let Some(response) = crate::profile::compat::native_bid(profile, bid) { native::validate_response_against_request( diff --git a/crates/rtblint-core/src/validator.rs b/crates/rtblint-core/src/validator.rs index 244e28a..6f2c25e 100644 --- a/crates/rtblint-core/src/validator.rs +++ b/crates/rtblint-core/src/validator.rs @@ -433,6 +433,11 @@ fn validate_known_object<'a>( for field in definition.fields.iter().filter(|field| field.required) { if !object.contains_key(field.name) && !crate::profile::compat::required_exception(profile, object_name, field.name, object) + && !(object_name == "Native" + && field.name == "request" + && object.iter().any(|(name, value)| { + dialect.native_field(object_name, name) && !value.is_null() + })) { issues.push(Issue { id: String::from("openrtb.field.required"), @@ -455,6 +460,21 @@ fn validate_known_object<'a>( logical_segments.push(field_name.as_str()); let parent_path_length = push_path_segment(instance_path, field_name); + if dialect.native_field(object_name, field_name) { + crate::native::validate_proto_alternative( + object_name, + object, + field_name, + value, + instance_path, + profile.native_request_asset_id_required(), + issues, + ); + instance_path.truncate(parent_path_length); + logical_segments.pop(); + continue; + } + if object_name == "Imp" && field_name == "native" && (profile == Profile::CommerceGrid @@ -944,6 +964,24 @@ fn validate_object_semantics( "EID" => validate_eid_semantics(object, instance_path, object_section, issues), "UID" => validate_uid_semantics(object, instance_path, object_section, issues), "UserAgent" => validate_user_agent_semantics(object, instance_path, object_section, issues), + "Site" if !adcom_object => { + if let Some(domain) = object.get("domain").and_then(Value::as_str) { + if domain.contains(['/', ':', '?', '#', '@']) + || domain.chars().any(char::is_whitespace) + { + issues.push(Issue { + id: String::from("openrtb.site.domain_not_domain"), + severity: Severity::Warning, + message: String::from( + "site.domain must contain a bare domain, such as publisher.example. \ + Put the full page URL in site.page.", + ), + path: Some(join_instance_path(instance_path, "domain")), + section: Some(String::from(object_section)), + }); + } + } + } "SupplyChain" => { validate_supply_chain_semantics(object, instance_path, object_section, issues) } diff --git a/crates/rtblint-core/tests/dialect.rs b/crates/rtblint-core/tests/dialect.rs index 06b9e83..d1223c2 100644 --- a/crates/rtblint-core/tests/dialect.rs +++ b/crates/rtblint-core/tests/dialect.rs @@ -12,6 +12,151 @@ use rtblint_core::{ const VERSION: OpenRtbVersion = OpenRtbVersion::V2_6_202606; +fn native_request(field: &str) -> serde_json::Value { + serde_json::json!({ + "id": "r", "imp": [{"id": "i", "secure": true, "native": { + (field): {"ver": "1.2", "privacy": true, "assets": [ + {"id": 1, "required": true, "title": {"len": 40}} + ]} + }}] + }) +} + +#[test] +fn proto_native_objects_accept_both_names_and_retain_asset_checks() { + for field in ["request_native", "requestNative"] { + let mut payload = native_request(field); + let result = + validate_bid_request_with_dialect(VERSION, Dialect::ProtoJson, &payload.to_string()); + assert!(result.valid, "{field}: {:?}", result.issues); + assert!(result.issues.is_empty(), "{field}: {:?}", result.issues); + let spec = validate_bid_request_for_version(VERSION, &payload.to_string()); + assert!(has_issue( + &spec, + "openrtb.field.required", + "imp[0].native.request" + )); + assert!(has_issue( + &spec, + "openrtb.field.undefined", + &format!("imp[0].native.{field}") + )); + + payload["imp"][0]["native"][field]["assets"][0]["title"] = serde_json::json!({}); + let result = + validate_bid_request_with_dialect(VERSION, Dialect::ProtoJson, &payload.to_string()); + assert!(has_issue( + &result, + "openrtb.native.field_required", + &format!("imp[0].native.{field}.assets[0].title.len") + )); + } +} + +#[test] +fn proto_native_rejects_bad_shapes_integer_flags_and_oneof_conflicts() { + let mut payload = native_request("request_native"); + payload["imp"][0]["native"]["request_native"] = serde_json::json!("{}"); + let result = + validate_bid_request_with_dialect(VERSION, Dialect::ProtoJson, &payload.to_string()); + assert!(has_issue( + &result, + "openrtb.type.mismatch", + "imp[0].native.request_native" + )); + payload = native_request("request_native"); + payload["imp"][0]["native"]["request_native"]["assets"][0]["required"] = serde_json::json!(1); + let result = + validate_bid_request_with_dialect(VERSION, Dialect::ProtoJson, &payload.to_string()); + assert!(has_issue( + &result, + "openrtb.dialect.integer_for_bool", + "imp[0].native.request_native.assets[0].required" + )); + payload["imp"][0]["native"]["request"] = serde_json::json!("{}"); + let result = + validate_bid_request_with_dialect(VERSION, Dialect::ProtoJson, &payload.to_string()); + assert!(has_issue( + &result, + "openrtb.dialect.oneof_conflict", + "imp[0].native.request_native" + )); +} + +#[test] +fn proto_native_responses_preserve_required_asset_pairing() { + use rtblint_core::{ + validate_bid_response_against_request_with_dialect, validate_bid_response_with_dialect, + }; + for field in ["adm_native", "admNative"] { + let request = native_request("requestNative").to_string(); + let mut response = serde_json::json!({"id": "r", "seatbid": [{"bid": [{ + "id": "b", "impid": "i", "price": 1.0, "mtype": 4, + (field): {"ver": "1.2", "link": {"url": "https://advertiser.example"}, "assets": [ + {"id": 1, "required": true, "title": {"text": "Example"}} + ]} + }]}]}); + let result = validate_bid_response_against_request_with_dialect( + VERSION, + Dialect::ProtoJson, + &request, + &response.to_string(), + ); + assert!(result.valid, "{field}: {:?}", result.issues); + response["seatbid"][0]["bid"][0][field]["assets"][0]["id"] = serde_json::json!(2); + let result = validate_bid_response_against_request_with_dialect( + VERSION, + Dialect::ProtoJson, + &request, + &response.to_string(), + ); + assert!(has_issue( + &result, + "openrtb.native.asset.required_missing", + &format!("seatbid[0].bid[0].{field}") + )); + response["seatbid"][0]["bid"][0][field] + .as_object_mut() + .unwrap() + .remove("link"); + let result = + validate_bid_response_with_dialect(VERSION, Dialect::ProtoJson, &response.to_string()); + assert!(has_issue( + &result, + "openrtb.native.field_required", + &format!("seatbid[0].bid[0].{field}.link.url") + )); + } +} + +#[test] +fn site_domain_warns_on_url_syntax_without_rejecting_bare_domains() { + for domain in [ + "https://publisher.example/", + "publisher.example/path", + "publisher.example:443", + "publisher.example?x=1", + " publisher.example", + ] { + let payload = serde_json::json!({"id":"r", "imp":[{"id":"i", "banner":{}}], "site":{"domain": domain}}); + let result = validate_bid_request_for_version(VERSION, &payload.to_string()); + assert!( + has_issue(&result, "openrtb.site.domain_not_domain", "site.domain"), + "{domain}" + ); + } + for domain in [ + "publisher.example", + "news.publisher.example", + "xn--bcher-kva.example", + "PUBLISHER.example", + ] { + let payload = serde_json::json!({"id":"r", "imp":[{"id":"i", "banner":{}}], "site":{"domain": domain}}); + let result = validate_bid_request_for_version(VERSION, &payload.to_string()); + assert!(result.issues.is_empty(), "{domain}: {:?}", result.issues); + } +} + fn has_issue(result: &ValidationResult, id: &str, path: &str) -> bool { result .issues diff --git a/crates/rtblint-core/tests/dialect_fixtures.rs b/crates/rtblint-core/tests/dialect_fixtures.rs new file mode 100644 index 0000000..8a4c53e --- /dev/null +++ b/crates/rtblint-core/tests/dialect_fixtures.rs @@ -0,0 +1,85 @@ +use std::{collections::HashSet, fs, path::Path}; + +use rtblint_core::{ + validate_bid_request_with_dialect, validate_bid_response_against_request_with_dialect, + validate_bid_response_with_dialect, Dialect, OpenRtbVersion, +}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +struct ExpectedIssue { + id: String, + severity: String, + path: Option, +} + +#[derive(Deserialize)] +struct Case { + name: String, + mode: String, + version: String, + dialect: String, + payload: String, + request: Option, + valid: bool, + issues: Vec, +} + +#[test] +fn stored_dialect_regressions_match_exact_findings() { + let directory = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/dialects"); + let cases: Vec = serde_json::from_str(include_str!("fixtures/dialects/cases.json")) + .expect("valid fixture manifest"); + let mut names = HashSet::new(); + let mut referenced = HashSet::new(); + for mut case in cases { + assert!(names.insert(case.name.clone()), "duplicate fixture name"); + referenced.insert(case.payload.clone()); + let payload = fs::read_to_string(directory.join(&case.payload)).expect("fixture payload"); + let version = OpenRtbVersion::from_id(&case.version).expect("supported fixture version"); + let dialect = Dialect::from_id(&case.dialect).expect("supported fixture dialect"); + let result = match case.mode.as_str() { + "request" => validate_bid_request_with_dialect(version, dialect, &payload), + "response" => validate_bid_response_with_dialect(version, dialect, &payload), + "pair" => { + let file = case.request.as_ref().expect("paired request file"); + referenced.insert(file.clone()); + let request = fs::read_to_string(directory.join(file)).expect("paired request"); + validate_bid_response_against_request_with_dialect( + version, dialect, &request, &payload, + ) + } + mode => panic!("unsupported fixture mode: {mode}"), + }; + assert_eq!( + result.valid, case.valid, + "{}: {:?}", + case.name, result.issues + ); + let mut actual: Vec<_> = result + .issues + .into_iter() + .map(|issue| ExpectedIssue { + id: issue.id, + severity: issue.severity.as_str().to_owned(), + path: issue.path, + }) + .collect(); + actual.sort(); + case.issues.sort(); + assert_eq!(actual, case.issues, "{}", case.name); + } + for entry in fs::read_dir(directory).expect("fixture directory") { + let name = entry + .expect("fixture entry") + .file_name() + .into_string() + .expect("UTF-8 file name"); + if name.ends_with(".json") && name != "cases.json" || name.ends_with(".txt") { + assert!( + referenced.contains(&name), + "fixture is not exercised: {name}" + ); + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/README.md b/crates/rtblint-core/tests/fixtures/dialects/README.md new file mode 100644 index 0000000..fcf3a4a --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/README.md @@ -0,0 +1,18 @@ +# Dialect fixtures preserve the native object regression cases. + +These synthetic payloads cover the shapes found in tester usage without +copying visitor data or identifiers. `cases.json` pins the version, dialect, +validation mode, validity, and complete finding IDs, severities and paths. +The fixture runner rejects extra findings as well as missing findings. + +Coverage includes protobuf native requests and responses under both field +spellings, boolean native flags, oneof conflicts, null alternatives, malformed +objects, required assets in paired responses, and encoded string alternatives. +The mixed request must fail under both dialects for the appropriate fields. +Site domain, legacy GDPR, invalid JSON, and zero-valued video skip controls +keep real payload errors distinct from validator regressions. + +The wire alternatives and boolean fields follow the [IAB protobuf schema](https://github.com/IABTechLab/openrtb-proto-v2/blob/master/openrtb-core/src/main/protobuf/openrtb.proto). +The site domain control follows [OpenRTB 2.6, section 3.2.13](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#3213---object-site). + +Run `cargo test -p rtblint-core --test dialect_fixtures`. diff --git a/crates/rtblint-core/tests/fixtures/dialects/cases.json b/crates/rtblint-core/tests/fixtures/dialects/cases.json new file mode 100644 index 0000000..beb3056 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/cases.json @@ -0,0 +1,455 @@ +[ + { + "name": "protobuf native request", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-request.json", + "valid": true, + "issues": [] + }, + { + "name": "protobuf camelCase native request", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-request-camel.json", + "valid": true, + "issues": [] + }, + { + "name": "spec rejects protobuf native request", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "native-proto-request.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "device.sua.mobile" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "imp[0].secure" + }, + { + "id": "openrtb.field.required", + "severity": "error", + "path": "imp[0].native.request" + }, + { + "id": "openrtb.field.undefined", + "severity": "error", + "path": "imp[0].native.request_native" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "site.mobile" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "source.schain.complete" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "source.schain.nodes[0].hp" + } + ] + }, + { + "name": "mixed flags remain invalid in proto", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-mixed-request.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "source.schain.complete" + }, + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "source.schain.nodes[0].hp" + } + ] + }, + { + "name": "mixed flags remain invalid in spec", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "native-mixed-request.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "device.sua.mobile" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "imp[0].secure" + }, + { + "id": "openrtb.field.required", + "severity": "error", + "path": "imp[0].native.request" + }, + { + "id": "openrtb.field.undefined", + "severity": "error", + "path": "imp[0].native.request_native" + }, + { + "id": "openrtb.dialect.bool_for_integer", + "severity": "error", + "path": "site.mobile" + } + ] + }, + { + "name": "native object shape", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-wrong-shape.json", + "valid": false, + "issues": [ + { + "id": "openrtb.type.mismatch", + "severity": "error", + "path": "imp[0].native.request_native" + } + ] + }, + { + "name": "null native oneof leaves request missing", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-null.json", + "valid": false, + "issues": [ + { + "id": "openrtb.field.required", + "severity": "error", + "path": "imp[0].native.request" + } + ] + }, + { + "name": "request oneof conflict", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-oneof-conflict.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.oneof_conflict", + "severity": "error", + "path": "imp[0].native.request_native" + } + ] + }, + { + "name": "duplicate native aliases", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-duplicate-alias.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.oneof_conflict", + "severity": "error", + "path": "imp[0].native.request_native" + }, + { + "id": "openrtb.dialect.oneof_conflict", + "severity": "error", + "path": "imp[0].native.requestNative" + } + ] + }, + { + "name": "native flag types", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-wrong-flags.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "imp[0].native.request_native.privacy" + }, + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "imp[0].native.request_native.aurlsupport" + }, + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "imp[0].native.request_native.assets[0].required" + }, + { + "id": "openrtb.type.mismatch", + "severity": "error", + "path": "imp[0].native.request_native.durlsupport" + } + ] + }, + { + "name": "native title length required", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-request-missing-title-length.json", + "valid": false, + "issues": [ + { + "id": "openrtb.native.field_required", + "severity": "error", + "path": "imp[0].native.request_native.assets[0].title.len" + } + ] + }, + { + "name": "spec string request remains valid", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "native-spec-request.json", + "valid": true, + "issues": [] + }, + { + "name": "proto accepts encoded string alternative", + "mode": "request", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-spec-request.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "imp[0].secure" + } + ] + }, + { + "name": "protobuf native response", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-response.json", + "valid": true, + "issues": [] + }, + { + "name": "protobuf camelCase native response", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-response-camel.json", + "valid": true, + "issues": [] + }, + { + "name": "native response link required", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-response-missing-link.json", + "valid": false, + "issues": [ + { + "id": "openrtb.native.field_required", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native.link.url" + } + ] + }, + { + "name": "native markup media type", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-response-wrong-media.json", + "valid": false, + "issues": [ + { + "id": "openrtb.bid.adm.media_type_mismatch", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native" + } + ] + }, + { + "name": "response oneof conflict", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-response-oneof-conflict.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.oneof_conflict", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native" + } + ] + }, + { + "name": "response native flag type", + "mode": "response", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-response-wrong-flag.json", + "valid": false, + "issues": [ + { + "id": "openrtb.dialect.integer_for_bool", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native.assets[0].required" + } + ] + }, + { + "name": "valid native pair", + "mode": "pair", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-response.json", + "valid": true, + "issues": [], + "request": "native-proto-request.json" + }, + { + "name": "valid camelCase native pair", + "mode": "pair", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-proto-response-camel.json", + "valid": true, + "issues": [], + "request": "native-proto-request-camel.json" + }, + { + "name": "required boolean asset pairing", + "mode": "pair", + "version": "2.6-202606", + "dialect": "proto-json", + "payload": "native-response-missing-required-asset.json", + "valid": false, + "issues": [ + { + "id": "openrtb.native.asset.required_missing", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native" + }, + { + "id": "openrtb.native.asset.id_unknown", + "severity": "error", + "path": "seatbid[0].bid[0].adm_native.assets[0].id" + } + ], + "request": "native-proto-request.json" + }, + { + "name": "spec string pair remains valid", + "mode": "pair", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "native-spec-response.json", + "valid": true, + "issues": [], + "request": "native-spec-request.json" + }, + { + "name": "URL in site domain", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "site-domain-url.json", + "valid": true, + "issues": [ + { + "id": "openrtb.site.domain_not_domain", + "severity": "warning", + "path": "site.domain" + } + ] + }, + { + "name": "bare site domain", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "site-domain-bare.json", + "valid": true, + "issues": [] + }, + { + "name": "legacy GDPR stays moved on latest", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "legacy-gdpr-extension.json", + "valid": false, + "issues": [ + { + "id": "openrtb.field.moved", + "severity": "error", + "path": "regs.ext.gdpr" + } + ] + }, + { + "name": "legacy GDPR remains valid on 2.5", + "mode": "request", + "version": "2.5", + "dialect": "spec-json", + "payload": "legacy-gdpr-extension.json", + "valid": true, + "issues": [] + }, + { + "name": "zero skip flag stays valid", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "video-skip-zero.json", + "valid": true, + "issues": [] + }, + { + "name": "malformed JSON stays invalid", + "mode": "request", + "version": "2.6-202606", + "dialect": "spec-json", + "payload": "invalid-json.txt", + "valid": false, + "issues": [ + { + "id": "openrtb.payload.invalid_json", + "severity": "error", + "path": null + } + ] + } +] diff --git a/crates/rtblint-core/tests/fixtures/dialects/invalid-json.txt b/crates/rtblint-core/tests/fixtures/dialects/invalid-json.txt new file mode 100644 index 0000000..41feb28 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/invalid-json.txt @@ -0,0 +1 @@ +{"id":"r","imp":[{"id":"i","banner":{}}],} diff --git a/crates/rtblint-core/tests/fixtures/dialects/legacy-gdpr-extension.json b/crates/rtblint-core/tests/fixtures/dialects/legacy-gdpr-extension.json new file mode 100644 index 0000000..5f2a174 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/legacy-gdpr-extension.json @@ -0,0 +1,17 @@ +{ + "id": "r", + "imp": [ + { + "id": "i", + "banner": {} + } + ], + "site": { + "domain": "publisher.example" + }, + "regs": { + "ext": { + "gdpr": 1 + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-mixed-request.json b/crates/rtblint-core/tests/fixtures/dialects/native-mixed-request.json new file mode 100644 index 0000000..8da9082 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-mixed-request.json @@ -0,0 +1,59 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": 1 + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-proto-request-camel.json b/crates/rtblint-core/tests/fixtures/dialects/native-proto-request-camel.json new file mode 100644 index 0000000..65a8afd --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-proto-request-camel.json @@ -0,0 +1,59 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "requestNative": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-proto-request.json b/crates/rtblint-core/tests/fixtures/dialects/native-proto-request.json new file mode 100644 index 0000000..d05a315 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-proto-request.json @@ -0,0 +1,59 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-proto-response-camel.json b/crates/rtblint-core/tests/fixtures/dialects/native-proto-response-camel.json new file mode 100644 index 0000000..cca50ed --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-proto-response-camel.json @@ -0,0 +1,30 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "admNative": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": true, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-proto-response.json b/crates/rtblint-core/tests/fixtures/dialects/native-proto-response.json new file mode 100644 index 0000000..0bf0d3a --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-proto-response.json @@ -0,0 +1,30 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": true, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-duplicate-alias.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-duplicate-alias.json new file mode 100644 index 0000000..570b6e2 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-duplicate-alias.json @@ -0,0 +1,74 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + }, + "requestNative": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-missing-title-length.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-missing-title-length.json new file mode 100644 index 0000000..3ac2daa --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-missing-title-length.json @@ -0,0 +1,57 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": {} + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-null.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-null.json new file mode 100644 index 0000000..ecd8fce --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-null.json @@ -0,0 +1,45 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": null + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-oneof-conflict.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-oneof-conflict.json new file mode 100644 index 0000000..fbeccaa --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-oneof-conflict.json @@ -0,0 +1,60 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": true, + "aurlsupport": false, + "durlsupport": false, + "assets": [ + { + "id": 1, + "required": true, + "title": { + "len": 40 + } + } + ] + }, + "request": "{\"ver\": \"1.2\", \"assets\": [{\"id\": 1, \"required\": 1, \"title\": {\"len\": 40}}]}" + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-flags.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-flags.json new file mode 100644 index 0000000..fee86b3 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-flags.json @@ -0,0 +1,59 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": { + "ver": "1.2", + "privacy": 1, + "aurlsupport": 0, + "durlsupport": "false", + "assets": [ + { + "id": 1, + "required": 1, + "title": { + "len": 40 + } + } + ] + } + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-shape.json b/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-shape.json new file mode 100644 index 0000000..8d33768 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-request-wrong-shape.json @@ -0,0 +1,45 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": true, + "native": { + "request_native": "{}" + } + } + ], + "site": { + "domain": "publisher.example", + "mobile": false + }, + "device": { + "sua": { + "mobile": true, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "130", + "0", + "0", + "0" + ] + } + ] + } + }, + "source": { + "schain": { + "ver": "1.0", + "complete": true, + "nodes": [ + { + "asi": "exchange.example", + "sid": "seller", + "hp": true + } + ] + } + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-link.json b/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-link.json new file mode 100644 index 0000000..d0ff1eb --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-link.json @@ -0,0 +1,27 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": true, + "title": { + "text": "Example" + } + } + ] + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-required-asset.json b/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-required-asset.json new file mode 100644 index 0000000..bdc56dd --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-response-missing-required-asset.json @@ -0,0 +1,30 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 2, + "required": true, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-response-oneof-conflict.json b/crates/rtblint-core/tests/fixtures/dialects/native-response-oneof-conflict.json new file mode 100644 index 0000000..a76a292 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-response-oneof-conflict.json @@ -0,0 +1,31 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": true, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + }, + "adm": "{\"ver\": \"1.2\", \"assets\": [{\"id\": 1, \"title\": {\"text\": \"Example\"}}], \"link\": {\"url\": \"https://advertiser.example\"}}" + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-flag.json b/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-flag.json new file mode 100644 index 0000000..30bacce --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-flag.json @@ -0,0 +1,30 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": 1, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-media.json b/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-media.json new file mode 100644 index 0000000..45fc515 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-response-wrong-media.json @@ -0,0 +1,30 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 1, + "adm_native": { + "ver": "1.2", + "assets": [ + { + "id": 1, + "required": true, + "title": { + "text": "Example" + } + } + ], + "link": { + "url": "https://advertiser.example" + } + } + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-spec-request.json b/crates/rtblint-core/tests/fixtures/dialects/native-spec-request.json new file mode 100644 index 0000000..d068855 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-spec-request.json @@ -0,0 +1,12 @@ +{ + "id": "native-r", + "imp": [ + { + "id": "i", + "secure": 1, + "native": { + "request": "{\"ver\": \"1.2\", \"assets\": [{\"id\": 1, \"required\": 1, \"title\": {\"len\": 40}}]}" + } + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/native-spec-response.json b/crates/rtblint-core/tests/fixtures/dialects/native-spec-response.json new file mode 100644 index 0000000..9121d1b --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/native-spec-response.json @@ -0,0 +1,16 @@ +{ + "id": "native-r", + "seatbid": [ + { + "bid": [ + { + "id": "b", + "impid": "i", + "price": 1.2, + "mtype": 4, + "adm": "{\"ver\": \"1.2\", \"assets\": [{\"id\": 1, \"required\": 1, \"title\": {\"text\": \"Example\"}}], \"link\": {\"url\": \"https://advertiser.example\"}}" + } + ] + } + ] +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/site-domain-bare.json b/crates/rtblint-core/tests/fixtures/dialects/site-domain-bare.json new file mode 100644 index 0000000..f3cb0ab --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/site-domain-bare.json @@ -0,0 +1,12 @@ +{ + "id": "r", + "imp": [ + { + "id": "i", + "banner": {} + } + ], + "site": { + "domain": "publisher.example" + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/site-domain-url.json b/crates/rtblint-core/tests/fixtures/dialects/site-domain-url.json new file mode 100644 index 0000000..24aa1bc --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/site-domain-url.json @@ -0,0 +1,12 @@ +{ + "id": "r", + "imp": [ + { + "id": "i", + "banner": {} + } + ], + "site": { + "domain": "https://publisher.example/article?x=1" + } +} diff --git a/crates/rtblint-core/tests/fixtures/dialects/video-skip-zero.json b/crates/rtblint-core/tests/fixtures/dialects/video-skip-zero.json new file mode 100644 index 0000000..763f4a6 --- /dev/null +++ b/crates/rtblint-core/tests/fixtures/dialects/video-skip-zero.json @@ -0,0 +1,14 @@ +{ + "id": "r", + "imp": [ + { + "id": "i", + "video": { + "mimes": [ + "video/mp4" + ], + "skip": 0 + } + } + ] +} diff --git a/crates/rtblint-grpc/Cargo.toml b/crates/rtblint-grpc/Cargo.toml index 9f46b34..ede411f 100644 --- a/crates/rtblint-grpc/Cargo.toml +++ b/crates/rtblint-grpc/Cargo.toml @@ -17,7 +17,7 @@ name = "rtblint-grpc" path = "src/main.rs" [dependencies] -rtblint-core = { path = "../rtblint-core", version = "0.14.2" } +rtblint-core = { path = "../rtblint-core", version = "0.14.3" } tonic = "0.14" tonic-prost = "0.14" diff --git a/crates/rtblint-mcp/Cargo.toml b/crates/rtblint-mcp/Cargo.toml index abddf06..c25e409 100644 --- a/crates/rtblint-mcp/Cargo.toml +++ b/crates/rtblint-mcp/Cargo.toml @@ -17,5 +17,5 @@ name = "rtblint-mcp" path = "src/main.rs" [dependencies] -rtblint-core = { path = "../rtblint-core", version = "0.14.2" } +rtblint-core = { path = "../rtblint-core", version = "0.14.3" } serde_json = "1" diff --git a/crates/rtblint-mcp/server.json b/crates/rtblint-mcp/server.json index 81079fa..241c680 100644 --- a/crates/rtblint-mcp/server.json +++ b/crates/rtblint-mcp/server.json @@ -10,7 +10,7 @@ }, "documentation": "https://rtblint.org/docs/mcp/", "license": "Apache-2.0", - "version": "0.14.0", + "version": "0.14.3", "remotes": [ { "type": "streamable-http", diff --git a/crates/rtblint-resolve/Cargo.toml b/crates/rtblint-resolve/Cargo.toml index 85a0122..c8ba64f 100644 --- a/crates/rtblint-resolve/Cargo.toml +++ b/crates/rtblint-resolve/Cargo.toml @@ -13,6 +13,6 @@ categories = ["development-tools"] readme = "../../README.md" [dependencies] -rtblint-core = { path = "../rtblint-core", version = "0.14.2" } +rtblint-core = { path = "../rtblint-core", version = "0.14.3" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/rtblint-wasm/Cargo.toml b/crates/rtblint-wasm/Cargo.toml index d7ba36c..d73d761 100644 --- a/crates/rtblint-wasm/Cargo.toml +++ b/crates/rtblint-wasm/Cargo.toml @@ -15,7 +15,7 @@ categories = ["parser-implementations", "wasm"] crate-type = ["cdylib"] [dependencies] -rtblint-core = { path = "../rtblint-core", version = "0.14.2" } +rtblint-core = { path = "../rtblint-core", version = "0.14.3" } wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" diff --git a/crates/rtblint/Cargo.toml b/crates/rtblint/Cargo.toml index 586bde8..e1349a4 100644 --- a/crates/rtblint/Cargo.toml +++ b/crates/rtblint/Cargo.toml @@ -17,7 +17,7 @@ name = "rtblint" path = "src/main.rs" [dependencies] -rtblint-core = { path = "../rtblint-core", version = "0.14.2" } -rtblint-resolve = { path = "../rtblint-resolve", version = "0.14.2" } +rtblint-core = { path = "../rtblint-core", version = "0.14.3" } +rtblint-resolve = { path = "../rtblint-resolve", version = "0.14.3" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/npm/package.json b/npm/package.json index 89eb242..34292b0 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "rtblint-core", - "version": "0.14.2", + "version": "0.14.3", "description": "OpenRTB linter: validates OpenRTB 2.x bid requests and bid responses against versioned IAB spec snapshots", "license": "Apache-2.0", "author": "Aleks ", diff --git a/npm/wasm/rtblint_wasm_bg.wasm b/npm/wasm/rtblint_wasm_bg.wasm index 4d0240f..78a52bd 100644 Binary files a/npm/wasm/rtblint_wasm_bg.wasm and b/npm/wasm/rtblint_wasm_bg.wasm differ