Rust types for Spur APIs including the Context API and Monocle.
Spur provides IP intelligence including VPN/proxy detection, geolocation, risk assessment, and infrastructure classification. This crate offers strongly-typed, serde-compatible data structures for working with Spur API responses.
- Multi-API support - Context API and Monocle in one crate
- Strongly typed enums with
Other(String)fallback for forward compatibility - Zero-copy deserialization with serde
- All fields optional - handles partial API responses gracefully
- Efficient serialization -
Nonevalues are omitted - Test utilities - builders and fixtures for testing (via
test-utilsfeature) - Property-based testing - proptest strategies included
Add to your Cargo.toml:
[dependencies]
spur = "0.3"
serde_json = "1"| Module | Purpose | Documentation |
|---|---|---|
spur::context |
IP intelligence via Context API | docs.spur.us/context-api |
spur::monocle |
Device-level VPN/proxy detection | docs.spur.us/monocle |
use spur::{IpContext, Infrastructure, TunnelType, Risk};
let json = r#"{
"ip": "89.39.106.191",
"infrastructure": "DATACENTER",
"as": { "number": 49981, "organization": "WorldStream" },
"risks": ["TUNNEL", "SPAM"],
"tunnels": [{ "type": "VPN", "operator": "NordVPN", "anonymous": true }]
}"#;
let context: IpContext = serde_json::from_str(json).unwrap();
// Use strongly typed enums
assert_eq!(context.infrastructure, Some(Infrastructure::Datacenter));
// Check for VPN usage
let is_vpn = context.tunnels.as_ref()
.map(|t| t.iter().any(|t| t.tunnel_type == Some(TunnelType::Vpn)))
.unwrap_or(false);
// Check risk factors
let has_tunnel_risk = context.risks.as_ref()
.map(|r| r.contains(&Risk::Tunnel))
.unwrap_or(false);use spur::monocle::Assessment;
// After calling the Monocle Decryption API
let json = r#"{
"vpn": true,
"proxied": false,
"anon": true,
"ip": "37.19.221.165",
"ts": "2022-12-01T01:00:50Z",
"complete": true,
"id": "0a3e401a-b0d5-496b-b1ff-6cb8eca542a2",
"sid": "checkout-form"
}"#;
let assessment: Assessment = serde_json::from_str(json).unwrap();
if assessment.is_anonymized() {
println!("User at {} is using anonymization", assessment.ip);
}
if !assessment.is_trustworthy() {
println!("Assessment incomplete, results may be unreliable");
}All API string fields that represent discrete values are typed enums:
| Enum | Values | Field |
|---|---|---|
Infrastructure |
Datacenter, Residential, Mobile, Business |
infrastructure |
Risk |
Tunnel, Spam, CallbackProxy, GeoMismatch |
risks |
Service |
OpenVpn, Ipsec, Wireguard, Ssh, Pptp |
services |
TunnelType |
Vpn, Proxy, Tor |
tunnel_type |
Behavior |
FileSharing, TorProxyUser |
behaviors |
DeviceType |
Mobile, Desktop |
types |
All enums include an Other(String) variant for forward compatibility with new API values:
use spur::Infrastructure;
// Unknown values deserialize to Other
let json = r#""SATELLITE""#;
let infra: Infrastructure = serde_json::from_str(json).unwrap();
assert!(infra.is_other());
assert_eq!(infra.as_str(), "SATELLITE");The main IP context object:
| Field | Type | Description |
|---|---|---|
ip |
String |
IPv4/IPv6 address |
infrastructure |
Infrastructure |
Network type (datacenter, residential, etc.) |
organization |
String |
Organization assigned to the IP |
autonomous_system |
AutonomousSystem |
BGP AS information |
location |
Location |
Geolocation data |
risks |
Vec<Risk> |
Risk factors |
services |
Vec<Service> |
Services/protocols in use |
tunnels |
Vec<Tunnel> |
VPN/proxy/Tor information |
client |
Client |
Client behavior and device data |
ai |
Ai |
AI activity observed from this IP |
Metadata and metrics for a service tag.
API token status with active, queries_remaining, and service_tier fields.
Decrypted assessment from the Monocle Decryption API:
| Field | Type | Description |
|---|---|---|
vpn |
bool |
VPN detected |
proxied |
bool |
Proxy detected |
anon |
bool |
Anonymous connection |
ip |
String |
Detected IP address |
ts |
String |
Timestamp (ISO 8601) |
complete |
bool |
Assessment completed successfully |
id |
String |
Unique assessment ID |
sid |
String |
Session ID |
Helper methods:
is_anonymized()- Returnstrueif VPN, proxy, or anonymousis_trustworthy()- Returnstrueif assessment completed
Enable the test-utils feature for testing helpers:
[dev-dependencies]
spur = { version = "0.3", features = ["test-utils"] }use spur::test_utils::IpContextBuilder;
use spur::{Infrastructure, Risk, Service};
let context = IpContextBuilder::new()
.ip("1.2.3.4")
.infrastructure(Infrastructure::Datacenter)
.asn(12345, "Example Corp")
.vpn("NordVPN")
.add_risk(Risk::Tunnel)
.add_service(Service::OpenVpn)
.build();use spur::test_utils::AssessmentBuilder;
let assessment = AssessmentBuilder::new()
.ip("1.2.3.4")
.vpn(true)
.anon(true)
.session_id("checkout")
.build();use spur::test_utils::{fixtures, monocle_fixtures};
// Context API fixtures
let residential = fixtures::residential_ip();
let vpn = fixtures::vpn_ip();
let tor = fixtures::tor_exit_node();
let datacenter = fixtures::datacenter_ip();
let ai_scraper = fixtures::ai_scraper_ip();
// Monocle fixtures
let clean = monocle_fixtures::clean_assessment();
let vpn_detected = monocle_fixtures::vpn_assessment();
let proxy_detected = monocle_fixtures::proxy_assessment();Save JSON responses from the Spur API as fixtures for testing against real data:
# Save an API response as a fixture
curl -s "https://api.spur.us/v2/context/1.2.3.4" \
-H "Token: YOUR_API_TOKEN" \
| jq . > tests/fixtures/my_new_fixture.jsonAll JSON files in tests/fixtures/ are automatically tested for:
- Valid parsing to
IpContext - Round-trip serialization
- Type-specific validation (VPN fixtures have tunnels, etc.)
See tests/fixtures/README.md for naming conventions and details.
use proptest::prelude::*;
use spur::proptest_strategies::*;
proptest! {
#[test]
fn context_roundtrip(context in arb_ip_context()) {
let json = serde_json::to_string(&context).unwrap();
let parsed: IpContext = serde_json::from_str(&json).unwrap();
assert_eq!(context, parsed);
}
#[test]
fn assessment_roundtrip(assessment in arb_assessment()) {
let json = serde_json::to_string(&assessment).unwrap();
let parsed: Assessment = serde_json::from_str(&json).unwrap();
assert_eq!(assessment, parsed);
}
}Version 0.3.0 reorganizes the crate into modules but maintains backwards compatibility:
// Both of these work in v0.3:
use spur::IpContext; // Root re-export (backwards compatible)
use spur::context::IpContext; // Explicit module path
// New Monocle support:
use spur::monocle::Assessment;Version 0.2.0 introduced breaking changes with strongly typed enums:
// v0.1 (string-based)
assert_eq!(context.infrastructure.as_deref(), Some("DATACENTER"));
// v0.2+ (enum-based)
assert_eq!(context.infrastructure, Some(Infrastructure::Datacenter));Key changes:
infrastructure: Option<String>→Option<Infrastructure>risks: Option<Vec<String>>→Option<Vec<Risk>>services: Option<Vec<String>>→Option<Vec<Service>>tunnel_type: Option<String>→Option<TunnelType>behaviors: Option<Vec<String>>→Option<Vec<Behavior>>types: Option<Vec<String>>→Option<Vec<DeviceType>>
MIT