rust_license_key/models.rs
1//! Data models for license representation and constraints.
2//!
3//! This module defines the core data structures used to represent licenses,
4//! their constraints, and validation results. All structures are designed
5//! to be serializable, versioned, and extensible.
6
7use chrono::{DateTime, Utc};
8use semver::Version;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11
12// =============================================================================
13// License Format Version
14// =============================================================================
15
16/// Current version of the license format.
17///
18/// This version number is embedded in every license and used to ensure
19/// compatibility between license files and library versions.
20/// Increment this when making breaking changes to the license format.
21pub const LICENSE_FORMAT_VERSION: u32 = 1;
22
23/// Minimum supported license format version.
24///
25/// Licenses with versions below this will be rejected during parsing.
26pub const MIN_SUPPORTED_LICENSE_VERSION: u32 = 1;
27
28/// Maximum supported license format version.
29///
30/// Licenses with versions above this will be rejected during parsing.
31pub const MAX_SUPPORTED_LICENSE_VERSION: u32 = 1;
32
33// =============================================================================
34// License Payload
35// =============================================================================
36
37/// The core license payload containing all license information.
38///
39/// This structure holds all the data that defines a license, including
40/// identification, temporal constraints, and feature restrictions.
41/// It is serialized to JSON and then signed by the publisher.
42///
43/// # Security Note
44///
45/// The payload itself is not encrypted, only signed. Anyone with access
46/// to the license file can read its contents. Do not store secrets in
47/// the license payload.
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub struct LicensePayload {
50 /// Version of the license format for forward compatibility.
51 /// This allows the library to reject incompatible future formats.
52 #[serde(rename = "v")]
53 pub format_version: u32,
54
55 /// Unique identifier for this specific license.
56 /// Should be a UUID or similar unique identifier.
57 #[serde(rename = "id")]
58 pub license_id: String,
59
60 /// Identifier for the customer or organization this license is issued to.
61 #[serde(rename = "customer")]
62 pub customer_id: String,
63
64 /// Human-readable name of the customer or organization.
65 #[serde(rename = "customer_name", skip_serializing_if = "Option::is_none")]
66 pub customer_name: Option<String>,
67
68 /// Timestamp when this license was issued.
69 #[serde(rename = "issued_at")]
70 pub issued_at: DateTime<Utc>,
71
72 /// All constraints and restrictions applied to this license.
73 #[serde(rename = "constraints")]
74 pub constraints: LicenseConstraints,
75
76 /// Optional additional metadata as key-value pairs.
77 /// Useful for application-specific data that doesn't fit standard fields.
78 #[serde(rename = "metadata", skip_serializing_if = "Option::is_none")]
79 pub metadata: Option<HashMap<String, serde_json::Value>>,
80}
81
82impl LicensePayload {
83 /// Returns the license ID as a string slice.
84 pub fn id(&self) -> &str {
85 &self.license_id
86 }
87
88 /// Returns the customer ID as a string slice.
89 pub fn customer(&self) -> &str {
90 &self.customer_id
91 }
92
93 /// Checks if the license format version is supported by this library.
94 pub fn is_version_supported(&self) -> bool {
95 self.format_version >= MIN_SUPPORTED_LICENSE_VERSION
96 && self.format_version <= MAX_SUPPORTED_LICENSE_VERSION
97 }
98
99 // =========================================================================
100 // Custom Key/Value Getters
101 // =========================================================================
102
103 /// Gets a custom value from the license metadata by key.
104 ///
105 /// Returns `None` if the key doesn't exist or if no metadata is present.
106 ///
107 /// # Example
108 ///
109 /// ```
110 /// use rust_license_key::prelude::*;
111 ///
112 /// // After validating a license:
113 /// // if let Some(value) = payload.get_value("max_users") {
114 /// // println!("Max users: {}", value);
115 /// // }
116 /// ```
117 pub fn get_value(&self, key: &str) -> Option<&serde_json::Value> {
118 self.metadata.as_ref().and_then(|m| m.get(key))
119 }
120
121 /// Gets a custom value from the license metadata, or returns a default value.
122 ///
123 /// # Example
124 ///
125 /// ```
126 /// use rust_license_key::prelude::*;
127 /// use serde_json::json;
128 ///
129 /// // After validating a license:
130 /// // let max_users = payload.get_value_or("max_users", &json!(10));
131 /// ```
132 pub fn get_value_or<'a>(
133 &'a self,
134 key: &str,
135 default: &'a serde_json::Value,
136 ) -> &'a serde_json::Value {
137 self.get_value(key).unwrap_or(default)
138 }
139
140 /// Gets a string value from the license metadata.
141 ///
142 /// Returns `None` if the key doesn't exist or the value is not a string.
143 ///
144 /// # Example
145 ///
146 /// ```
147 /// use rust_license_key::prelude::*;
148 ///
149 /// // After validating a license:
150 /// // if let Some(tier) = payload.get_string("tier") {
151 /// // println!("License tier: {}", tier);
152 /// // }
153 /// ```
154 pub fn get_string(&self, key: &str) -> Option<&str> {
155 self.get_value(key).and_then(|v| v.as_str())
156 }
157
158 /// Gets a string value from the license metadata, or returns a default.
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// use rust_license_key::prelude::*;
164 ///
165 /// // After validating a license:
166 /// // let tier = payload.get_string_or("tier", "basic");
167 /// ```
168 pub fn get_string_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
169 self.get_string(key).unwrap_or(default)
170 }
171
172 /// Gets an i64 value from the license metadata.
173 ///
174 /// Returns `None` if the key doesn't exist or the value is not a number.
175 ///
176 /// # Example
177 ///
178 /// ```
179 /// use rust_license_key::prelude::*;
180 ///
181 /// // After validating a license:
182 /// // if let Some(max_users) = payload.get_i64("max_users") {
183 /// // println!("Max users: {}", max_users);
184 /// // }
185 /// ```
186 pub fn get_i64(&self, key: &str) -> Option<i64> {
187 self.get_value(key).and_then(|v| v.as_i64())
188 }
189
190 /// Gets an i64 value from the license metadata, or returns a default.
191 ///
192 /// # Example
193 ///
194 /// ```
195 /// use rust_license_key::prelude::*;
196 ///
197 /// // After validating a license:
198 /// // let max_users = payload.get_i64_or("max_users", 10);
199 /// ```
200 pub fn get_i64_or(&self, key: &str, default: i64) -> i64 {
201 self.get_i64(key).unwrap_or(default)
202 }
203
204 /// Gets a u64 value from the license metadata.
205 ///
206 /// Returns `None` if the key doesn't exist or the value is not a positive number.
207 pub fn get_u64(&self, key: &str) -> Option<u64> {
208 self.get_value(key).and_then(|v| v.as_u64())
209 }
210
211 /// Gets a u64 value from the license metadata, or returns a default.
212 pub fn get_u64_or(&self, key: &str, default: u64) -> u64 {
213 self.get_u64(key).unwrap_or(default)
214 }
215
216 /// Gets an f64 value from the license metadata.
217 ///
218 /// Returns `None` if the key doesn't exist or the value is not a number.
219 pub fn get_f64(&self, key: &str) -> Option<f64> {
220 self.get_value(key).and_then(|v| v.as_f64())
221 }
222
223 /// Gets an f64 value from the license metadata, or returns a default.
224 pub fn get_f64_or(&self, key: &str, default: f64) -> f64 {
225 self.get_f64(key).unwrap_or(default)
226 }
227
228 /// Gets a boolean value from the license metadata.
229 ///
230 /// Returns `None` if the key doesn't exist or the value is not a boolean.
231 pub fn get_bool(&self, key: &str) -> Option<bool> {
232 self.get_value(key).and_then(|v| v.as_bool())
233 }
234
235 /// Gets a boolean value from the license metadata, or returns a default.
236 pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
237 self.get_bool(key).unwrap_or(default)
238 }
239
240 /// Gets an array value from the license metadata.
241 ///
242 /// Returns `None` if the key doesn't exist or the value is not an array.
243 ///
244 /// # Example
245 ///
246 /// ```
247 /// use rust_license_key::prelude::*;
248 ///
249 /// // After validating a license:
250 /// // if let Some(modules) = payload.get_array("allowed_modules") {
251 /// // for module in modules {
252 /// // println!("Module: {}", module);
253 /// // }
254 /// // }
255 /// ```
256 pub fn get_array(&self, key: &str) -> Option<&Vec<serde_json::Value>> {
257 self.get_value(key).and_then(|v| v.as_array())
258 }
259
260 /// Gets a string array from the license metadata.
261 ///
262 /// Returns `None` if the key doesn't exist or the value is not an array.
263 /// Non-string elements in the array are filtered out.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use rust_license_key::prelude::*;
269 ///
270 /// // After validating a license:
271 /// // if let Some(modules) = payload.get_string_array("allowed_modules") {
272 /// // for module in modules {
273 /// // println!("Module: {}", module);
274 /// // }
275 /// // }
276 /// ```
277 pub fn get_string_array(&self, key: &str) -> Option<Vec<&str>> {
278 self.get_array(key)
279 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
280 }
281
282 /// Gets an object value from the license metadata.
283 ///
284 /// Returns `None` if the key doesn't exist or the value is not an object.
285 pub fn get_object(&self, key: &str) -> Option<&serde_json::Map<String, serde_json::Value>> {
286 self.get_value(key).and_then(|v| v.as_object())
287 }
288
289 /// Checks if a key exists in the license metadata.
290 ///
291 /// # Example
292 ///
293 /// ```
294 /// use rust_license_key::prelude::*;
295 ///
296 /// // After validating a license:
297 /// // if payload.has_key("enterprise_features") {
298 /// // // Enable enterprise features
299 /// // }
300 /// ```
301 pub fn has_key(&self, key: &str) -> bool {
302 self.metadata
303 .as_ref()
304 .map(|m| m.contains_key(key))
305 .unwrap_or(false)
306 }
307
308 /// Returns all metadata keys.
309 ///
310 /// Returns an empty iterator if no metadata is present.
311 pub fn keys(&self) -> impl Iterator<Item = &String> {
312 self.metadata.iter().flat_map(|m| m.keys())
313 }
314}
315
316// =============================================================================
317// License Constraints
318// =============================================================================
319
320/// All constraints and restrictions that can be applied to a license.
321///
322/// Each field is optional, allowing flexible license configurations.
323/// An absent constraint means "no restriction" for that aspect.
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
325pub struct LicenseConstraints {
326 /// Date and time when this license expires.
327 /// If `None`, the license never expires.
328 #[serde(rename = "expires_at", skip_serializing_if = "Option::is_none")]
329 pub expiration_date: Option<DateTime<Utc>>,
330
331 /// Date and time when this license becomes valid.
332 /// If `None`, the license is immediately valid upon issuance.
333 #[serde(rename = "valid_from", skip_serializing_if = "Option::is_none")]
334 pub valid_from: Option<DateTime<Utc>>,
335
336 /// Set of features or plugins that are explicitly allowed.
337 /// If `None`, no feature restrictions apply (all features allowed).
338 /// If `Some(empty_set)`, no features are allowed.
339 #[serde(rename = "allowed_features", skip_serializing_if = "Option::is_none")]
340 pub allowed_features: Option<HashSet<String>>,
341
342 /// Set of features or plugins that are explicitly denied.
343 /// Takes precedence over `allowed_features` if both are specified.
344 #[serde(rename = "denied_features", skip_serializing_if = "Option::is_none")]
345 pub denied_features: Option<HashSet<String>>,
346
347 /// Maximum number of concurrent connections or seats allowed.
348 /// If `None`, no connection limit applies.
349 #[serde(rename = "max_connections", skip_serializing_if = "Option::is_none")]
350 pub max_connections: Option<u32>,
351
352 /// Set of hostnames where this license can be used.
353 /// If `None`, no hostname restrictions apply.
354 #[serde(rename = "allowed_hostnames", skip_serializing_if = "Option::is_none")]
355 pub allowed_hostnames: Option<HashSet<String>>,
356
357 /// Set of machine identifiers where this license can be used.
358 /// Machine identifiers can be hardware IDs, container IDs, etc.
359 /// If `None`, no machine restrictions apply.
360 #[serde(
361 rename = "allowed_machine_ids",
362 skip_serializing_if = "Option::is_none"
363 )]
364 pub allowed_machine_ids: Option<HashSet<String>>,
365
366 /// Minimum software version required to use this license.
367 /// If `None`, no minimum version requirement.
368 #[serde(rename = "min_version", skip_serializing_if = "Option::is_none")]
369 pub minimum_software_version: Option<Version>,
370
371 /// Maximum software version allowed to use this license.
372 /// Useful for deprecating old licenses with newer software versions.
373 /// If `None`, no maximum version requirement.
374 #[serde(rename = "max_version", skip_serializing_if = "Option::is_none")]
375 pub maximum_software_version: Option<Version>,
376
377 /// Custom key-value constraints for application-specific validation.
378 /// The application is responsible for interpreting these constraints.
379 #[serde(rename = "custom", skip_serializing_if = "Option::is_none")]
380 pub custom_constraints: Option<HashMap<String, serde_json::Value>>,
381}
382
383impl LicenseConstraints {
384 /// Creates a new empty constraints object with no restrictions.
385 pub fn new() -> Self {
386 Self::default()
387 }
388
389 /// Checks if a feature is allowed by this license.
390 ///
391 /// # Logic
392 ///
393 /// 1. If the feature is in `denied_features`, it is not allowed.
394 /// 2. If `allowed_features` is `None`, the feature is allowed.
395 /// 3. If `allowed_features` is `Some`, the feature must be in the set.
396 pub fn is_feature_allowed(&self, feature: &str) -> bool {
397 // Check denied list first (takes precedence)
398 if let Some(ref denied) = self.denied_features {
399 if denied.contains(feature) {
400 return false;
401 }
402 }
403
404 // Check allowed list
405 match &self.allowed_features {
406 None => true, // No restrictions
407 Some(allowed) => allowed.contains(feature),
408 }
409 }
410
411 /// Checks if a hostname is allowed by this license.
412 ///
413 /// Returns `true` if no hostname restrictions exist or if the hostname
414 /// is in the allowed set.
415 pub fn is_hostname_allowed(&self, hostname: &str) -> bool {
416 match &self.allowed_hostnames {
417 None => true,
418 Some(allowed) => allowed.contains(hostname),
419 }
420 }
421
422 /// Checks if a machine identifier is allowed by this license.
423 ///
424 /// Returns `true` if no machine ID restrictions exist or if the machine ID
425 /// is in the allowed set.
426 pub fn is_machine_id_allowed(&self, machine_id: &str) -> bool {
427 match &self.allowed_machine_ids {
428 None => true,
429 Some(allowed) => allowed.contains(machine_id),
430 }
431 }
432
433 /// Checks if a software version is compatible with this license.
434 ///
435 /// Returns `Ok(())` if the version is within the allowed range,
436 /// or `Err` with a description of why it's incompatible.
437 pub fn check_version_compatibility(&self, version: &Version) -> Result<(), String> {
438 if let Some(ref min_version) = self.minimum_software_version {
439 if version < min_version {
440 return Err(format!(
441 "version {} is below minimum required version {}",
442 version, min_version
443 ));
444 }
445 }
446
447 if let Some(ref max_version) = self.maximum_software_version {
448 if version > max_version {
449 return Err(format!(
450 "version {} exceeds maximum allowed version {}",
451 version, max_version
452 ));
453 }
454 }
455
456 Ok(())
457 }
458}
459
460// =============================================================================
461// Signed License Container
462// =============================================================================
463
464/// A complete signed license ready for distribution.
465///
466/// This structure contains the license payload along with its cryptographic
467/// signature. It is what gets serialized and distributed to customers.
468///
469/// # Format
470///
471/// When serialized for distribution, this becomes a JSON object with:
472/// - `payload`: The base64-encoded JSON payload
473/// - `signature`: The base64-encoded Ed25519 signature
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct SignedLicense {
476 /// The license payload, base64-encoded JSON.
477 #[serde(rename = "payload")]
478 pub encoded_payload: String,
479
480 /// The Ed25519 signature of the payload, base64-encoded.
481 #[serde(rename = "signature")]
482 pub encoded_signature: String,
483}
484
485impl SignedLicense {
486 /// Creates a new signed license from encoded components.
487 ///
488 /// # Arguments
489 ///
490 /// * `encoded_payload` - Base64-encoded JSON payload
491 /// * `encoded_signature` - Base64-encoded signature bytes
492 pub fn new(encoded_payload: String, encoded_signature: String) -> Self {
493 Self {
494 encoded_payload,
495 encoded_signature,
496 }
497 }
498
499 /// Serializes this signed license to a JSON string for distribution.
500 ///
501 /// This is the format that should be saved to a file or transmitted
502 /// to the customer.
503 pub fn to_json(&self) -> Result<String, serde_json::Error> {
504 serde_json::to_string_pretty(self)
505 }
506
507 /// Deserializes a signed license from a JSON string.
508 ///
509 /// This is how license files are loaded for validation.
510 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
511 serde_json::from_str(json)
512 }
513}
514
515// =============================================================================
516// Validation Context
517// =============================================================================
518
519/// Context information provided during license validation.
520///
521/// This structure contains all the runtime information needed to validate
522/// a license against the current execution environment.
523#[derive(Debug, Clone, Default)]
524pub struct ValidationContext {
525 /// The current date and time to check against temporal constraints.
526 /// If `None`, uses the system's current time.
527 pub current_time: Option<DateTime<Utc>>,
528
529 /// The current hostname to check against hostname constraints.
530 pub current_hostname: Option<String>,
531
532 /// The current machine identifier to check against machine ID constraints.
533 pub current_machine_id: Option<String>,
534
535 /// The current software version to check against version constraints.
536 pub current_software_version: Option<Version>,
537
538 /// The current number of connections to check against connection limits.
539 pub current_connection_count: Option<u32>,
540
541 /// Features being requested for this validation.
542 /// Each feature will be checked against the license constraints.
543 pub requested_features: Vec<String>,
544
545 /// Custom values to check against custom constraints.
546 pub custom_values: HashMap<String, serde_json::Value>,
547}
548
549impl ValidationContext {
550 /// Creates a new empty validation context.
551 pub fn new() -> Self {
552 Self::default()
553 }
554
555 /// Sets the current time for validation.
556 pub fn with_time(mut self, time: DateTime<Utc>) -> Self {
557 self.current_time = Some(time);
558 self
559 }
560
561 /// Sets the current hostname for validation.
562 pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
563 self.current_hostname = Some(hostname.into());
564 self
565 }
566
567 /// Sets the current machine identifier for validation.
568 pub fn with_machine_id(mut self, machine_id: impl Into<String>) -> Self {
569 self.current_machine_id = Some(machine_id.into());
570 self
571 }
572
573 /// Sets the current software version for validation.
574 pub fn with_software_version(mut self, version: Version) -> Self {
575 self.current_software_version = Some(version);
576 self
577 }
578
579 /// Sets the current connection count for validation.
580 pub fn with_connection_count(mut self, count: u32) -> Self {
581 self.current_connection_count = Some(count);
582 self
583 }
584
585 /// Adds a requested feature to check against the license.
586 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
587 self.requested_features.push(feature.into());
588 self
589 }
590
591 /// Adds multiple requested features to check against the license.
592 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
593 self.requested_features
594 .extend(features.into_iter().map(Into::into));
595 self
596 }
597
598 /// Adds a custom value for constraint checking.
599 pub fn with_custom_value(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
600 self.custom_values.insert(key.into(), value);
601 self
602 }
603}
604
605// =============================================================================
606// Validation Result
607// =============================================================================
608
609/// The result of validating a license.
610///
611/// This structure provides comprehensive information about the validation
612/// outcome, including whether it succeeded and detailed status information.
613#[derive(Debug, Clone)]
614pub struct ValidationResult {
615 /// Whether the license is valid.
616 pub is_valid: bool,
617
618 /// The validated license payload (only present if signature was valid).
619 pub payload: Option<LicensePayload>,
620
621 /// List of validation failures encountered.
622 /// Empty if validation succeeded.
623 pub failures: Vec<crate::error::ValidationFailure>,
624
625 /// Time remaining until the license expires.
626 /// `None` if the license never expires or is already expired.
627 pub time_remaining: Option<chrono::Duration>,
628
629 /// List of features that are allowed by this license.
630 /// Only populated if validation succeeded.
631 pub allowed_features: Option<HashSet<String>>,
632
633 /// List of features that are denied by this license.
634 /// Only populated if validation succeeded.
635 pub denied_features: Option<HashSet<String>>,
636}
637
638impl ValidationResult {
639 /// Creates a successful validation result.
640 pub fn success(payload: LicensePayload) -> Self {
641 let time_remaining = payload
642 .constraints
643 .expiration_date
644 .map(|exp| exp.signed_duration_since(Utc::now()));
645
646 let allowed_features = payload.constraints.allowed_features.clone();
647 let denied_features = payload.constraints.denied_features.clone();
648
649 Self {
650 is_valid: true,
651 payload: Some(payload),
652 failures: Vec::new(),
653 time_remaining,
654 allowed_features,
655 denied_features,
656 }
657 }
658
659 /// Creates a failed validation result with the given failures.
660 pub fn failure(failures: Vec<crate::error::ValidationFailure>) -> Self {
661 Self {
662 is_valid: false,
663 payload: None,
664 failures,
665 time_remaining: None,
666 allowed_features: None,
667 denied_features: None,
668 }
669 }
670
671 /// Adds a failure to the result and marks it as invalid.
672 pub fn add_failure(&mut self, failure: crate::error::ValidationFailure) {
673 self.is_valid = false;
674 self.failures.push(failure);
675 }
676
677 /// Returns true if the license is valid and not expired.
678 pub fn is_active(&self) -> bool {
679 self.is_valid
680 && self
681 .time_remaining
682 .map(|d| d.num_seconds() > 0)
683 .unwrap_or(true)
684 }
685
686 /// Returns the number of days remaining until expiration.
687 /// Returns `None` if the license never expires.
688 pub fn days_remaining(&self) -> Option<i64> {
689 self.time_remaining.map(|d| d.num_days())
690 }
691
692 /// Checks if a specific feature is allowed by the validated license.
693 pub fn is_feature_allowed(&self, feature: &str) -> bool {
694 if !self.is_valid {
695 return false;
696 }
697
698 // Check denied list first
699 if let Some(ref denied) = self.denied_features {
700 if denied.contains(feature) {
701 return false;
702 }
703 }
704
705 // Check allowed list
706 match &self.allowed_features {
707 None => true,
708 Some(allowed) => allowed.contains(feature),
709 }
710 }
711
712 // =========================================================================
713 // Custom Key/Value Getters (delegates to payload)
714 // =========================================================================
715
716 /// Gets a custom value from the license metadata by key.
717 ///
718 /// Returns `None` if validation failed, the key doesn't exist,
719 /// or if no metadata is present.
720 ///
721 /// # Example
722 ///
723 /// ```
724 /// use rust_license_key::prelude::*;
725 ///
726 /// // After validating:
727 /// // if let Some(value) = result.get_value("max_users") {
728 /// // println!("Max users: {}", value);
729 /// // }
730 /// ```
731 pub fn get_value(&self, key: &str) -> Option<&serde_json::Value> {
732 self.payload.as_ref().and_then(|p| p.get_value(key))
733 }
734
735 /// Gets a custom value from the license metadata, or returns a default value.
736 pub fn get_value_or<'a>(
737 &'a self,
738 key: &str,
739 default: &'a serde_json::Value,
740 ) -> &'a serde_json::Value {
741 self.get_value(key).unwrap_or(default)
742 }
743
744 /// Gets a string value from the license metadata.
745 ///
746 /// Returns `None` if validation failed, the key doesn't exist,
747 /// or the value is not a string.
748 pub fn get_string(&self, key: &str) -> Option<&str> {
749 self.payload.as_ref().and_then(|p| p.get_string(key))
750 }
751
752 /// Gets a string value from the license metadata, or returns a default.
753 pub fn get_string_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
754 self.get_string(key).unwrap_or(default)
755 }
756
757 /// Gets an i64 value from the license metadata.
758 ///
759 /// Returns `None` if validation failed, the key doesn't exist,
760 /// or the value is not a number.
761 pub fn get_i64(&self, key: &str) -> Option<i64> {
762 self.payload.as_ref().and_then(|p| p.get_i64(key))
763 }
764
765 /// Gets an i64 value from the license metadata, or returns a default.
766 pub fn get_i64_or(&self, key: &str, default: i64) -> i64 {
767 self.get_i64(key).unwrap_or(default)
768 }
769
770 /// Gets a u64 value from the license metadata.
771 ///
772 /// Returns `None` if validation failed, the key doesn't exist,
773 /// or the value is not a positive number.
774 pub fn get_u64(&self, key: &str) -> Option<u64> {
775 self.payload.as_ref().and_then(|p| p.get_u64(key))
776 }
777
778 /// Gets a u64 value from the license metadata, or returns a default.
779 pub fn get_u64_or(&self, key: &str, default: u64) -> u64 {
780 self.get_u64(key).unwrap_or(default)
781 }
782
783 /// Gets an f64 value from the license metadata.
784 ///
785 /// Returns `None` if validation failed, the key doesn't exist,
786 /// or the value is not a number.
787 pub fn get_f64(&self, key: &str) -> Option<f64> {
788 self.payload.as_ref().and_then(|p| p.get_f64(key))
789 }
790
791 /// Gets an f64 value from the license metadata, or returns a default.
792 pub fn get_f64_or(&self, key: &str, default: f64) -> f64 {
793 self.get_f64(key).unwrap_or(default)
794 }
795
796 /// Gets a boolean value from the license metadata.
797 ///
798 /// Returns `None` if validation failed, the key doesn't exist,
799 /// or the value is not a boolean.
800 pub fn get_bool(&self, key: &str) -> Option<bool> {
801 self.payload.as_ref().and_then(|p| p.get_bool(key))
802 }
803
804 /// Gets a boolean value from the license metadata, or returns a default.
805 pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
806 self.get_bool(key).unwrap_or(default)
807 }
808
809 /// Gets an array value from the license metadata.
810 ///
811 /// Returns `None` if validation failed, the key doesn't exist,
812 /// or the value is not an array.
813 pub fn get_array(&self, key: &str) -> Option<&Vec<serde_json::Value>> {
814 self.payload.as_ref().and_then(|p| p.get_array(key))
815 }
816
817 /// Gets a string array from the license metadata.
818 ///
819 /// Returns `None` if validation failed, the key doesn't exist,
820 /// or the value is not an array.
821 pub fn get_string_array(&self, key: &str) -> Option<Vec<&str>> {
822 self.payload.as_ref().and_then(|p| p.get_string_array(key))
823 }
824
825 /// Gets an object value from the license metadata.
826 ///
827 /// Returns `None` if validation failed, the key doesn't exist,
828 /// or the value is not an object.
829 pub fn get_object(&self, key: &str) -> Option<&serde_json::Map<String, serde_json::Value>> {
830 self.payload.as_ref().and_then(|p| p.get_object(key))
831 }
832
833 /// Checks if a key exists in the license metadata.
834 ///
835 /// Returns `false` if validation failed or no metadata is present.
836 pub fn has_key(&self, key: &str) -> bool {
837 self.payload
838 .as_ref()
839 .map(|p| p.has_key(key))
840 .unwrap_or(false)
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn test_license_constraints_feature_allowed() {
850 let mut constraints = LicenseConstraints::new();
851
852 // No restrictions - all features allowed
853 assert!(constraints.is_feature_allowed("any_feature"));
854
855 // With allowed list
856 constraints.allowed_features = Some(HashSet::from([
857 "feature_a".to_string(),
858 "feature_b".to_string(),
859 ]));
860 assert!(constraints.is_feature_allowed("feature_a"));
861 assert!(!constraints.is_feature_allowed("feature_c"));
862
863 // Denied list takes precedence
864 constraints.denied_features = Some(HashSet::from(["feature_a".to_string()]));
865 assert!(!constraints.is_feature_allowed("feature_a"));
866 assert!(constraints.is_feature_allowed("feature_b"));
867 }
868
869 #[test]
870 fn test_license_constraints_version_compatibility() {
871 let mut constraints = LicenseConstraints::new();
872 constraints.minimum_software_version = Some(Version::new(1, 0, 0));
873 constraints.maximum_software_version = Some(Version::new(2, 0, 0));
874
875 assert!(constraints
876 .check_version_compatibility(&Version::new(1, 5, 0))
877 .is_ok());
878 assert!(constraints
879 .check_version_compatibility(&Version::new(0, 9, 0))
880 .is_err());
881 assert!(constraints
882 .check_version_compatibility(&Version::new(2, 1, 0))
883 .is_err());
884 }
885
886 #[test]
887 fn test_validation_context_builder() {
888 let context = ValidationContext::new()
889 .with_hostname("server.example.com")
890 .with_software_version(Version::new(1, 2, 3))
891 .with_feature("premium")
892 .with_features(vec!["analytics", "reports"]);
893
894 assert_eq!(
895 context.current_hostname.as_deref(),
896 Some("server.example.com")
897 );
898 assert_eq!(
899 context.current_software_version,
900 Some(Version::new(1, 2, 3))
901 );
902 assert_eq!(context.requested_features.len(), 3);
903 }
904
905 #[test]
906 fn test_license_payload_version_check() {
907 let payload = LicensePayload {
908 format_version: LICENSE_FORMAT_VERSION,
909 license_id: "test".to_string(),
910 customer_id: "customer".to_string(),
911 customer_name: None,
912 issued_at: Utc::now(),
913 constraints: LicenseConstraints::new(),
914 metadata: None,
915 };
916
917 assert!(payload.is_version_supported());
918 }
919
920 #[test]
921 fn test_signed_license_json_roundtrip() {
922 let license = SignedLicense::new(
923 "encoded_payload".to_string(),
924 "encoded_signature".to_string(),
925 );
926
927 let json = license.to_json().unwrap();
928 let parsed = SignedLicense::from_json(&json).unwrap();
929
930 assert_eq!(license.encoded_payload, parsed.encoded_payload);
931 assert_eq!(license.encoded_signature, parsed.encoded_signature);
932 }
933
934 #[test]
935 fn test_license_payload_get_value() {
936 let mut metadata = HashMap::new();
937 metadata.insert("tier".to_string(), serde_json::json!("enterprise"));
938 metadata.insert("max_users".to_string(), serde_json::json!(100));
939 metadata.insert("is_beta".to_string(), serde_json::json!(true));
940 metadata.insert(
941 "modules".to_string(),
942 serde_json::json!(["core", "analytics"]),
943 );
944
945 let payload = LicensePayload {
946 format_version: LICENSE_FORMAT_VERSION,
947 license_id: "test".to_string(),
948 customer_id: "customer".to_string(),
949 customer_name: None,
950 issued_at: Utc::now(),
951 constraints: LicenseConstraints::new(),
952 metadata: Some(metadata),
953 };
954
955 // Test get_value
956 assert!(payload.get_value("tier").is_some());
957 assert!(payload.get_value("nonexistent").is_none());
958
959 // Test get_string
960 assert_eq!(payload.get_string("tier"), Some("enterprise"));
961 assert_eq!(payload.get_string("max_users"), None); // Not a string
962 assert_eq!(payload.get_string_or("tier", "basic"), "enterprise");
963 assert_eq!(payload.get_string_or("nonexistent", "default"), "default");
964
965 // Test get_i64
966 assert_eq!(payload.get_i64("max_users"), Some(100));
967 assert_eq!(payload.get_i64("tier"), None); // Not a number
968 assert_eq!(payload.get_i64_or("max_users", 50), 100);
969 assert_eq!(payload.get_i64_or("nonexistent", 50), 50);
970
971 // Test get_bool
972 assert_eq!(payload.get_bool("is_beta"), Some(true));
973 assert_eq!(payload.get_bool("tier"), None); // Not a bool
974 assert_eq!(payload.get_bool_or("is_beta", false), true);
975 assert_eq!(payload.get_bool_or("nonexistent", false), false);
976
977 // Test get_array
978 assert!(payload.get_array("modules").is_some());
979 assert_eq!(payload.get_array("modules").unwrap().len(), 2);
980
981 // Test get_string_array
982 let modules = payload.get_string_array("modules").unwrap();
983 assert_eq!(modules, vec!["core", "analytics"]);
984
985 // Test has_key
986 assert!(payload.has_key("tier"));
987 assert!(!payload.has_key("nonexistent"));
988
989 // Test keys
990 let keys: Vec<_> = payload.keys().collect();
991 assert_eq!(keys.len(), 4);
992 }
993
994 #[test]
995 fn test_license_payload_get_value_no_metadata() {
996 let payload = LicensePayload {
997 format_version: LICENSE_FORMAT_VERSION,
998 license_id: "test".to_string(),
999 customer_id: "customer".to_string(),
1000 customer_name: None,
1001 issued_at: Utc::now(),
1002 constraints: LicenseConstraints::new(),
1003 metadata: None,
1004 };
1005
1006 assert!(payload.get_value("any").is_none());
1007 assert_eq!(payload.get_string_or("any", "default"), "default");
1008 assert_eq!(payload.get_i64_or("any", 42), 42);
1009 assert!(!payload.has_key("any"));
1010 assert_eq!(payload.keys().count(), 0);
1011 }
1012
1013 #[test]
1014 fn test_validation_result_get_value() {
1015 let mut metadata = HashMap::new();
1016 metadata.insert("tier".to_string(), serde_json::json!("premium"));
1017 metadata.insert("limit".to_string(), serde_json::json!(500));
1018
1019 let payload = LicensePayload {
1020 format_version: LICENSE_FORMAT_VERSION,
1021 license_id: "test".to_string(),
1022 customer_id: "customer".to_string(),
1023 customer_name: None,
1024 issued_at: Utc::now(),
1025 constraints: LicenseConstraints::new(),
1026 metadata: Some(metadata),
1027 };
1028
1029 let result = ValidationResult::success(payload);
1030
1031 // Test getters on ValidationResult
1032 assert_eq!(result.get_string("tier"), Some("premium"));
1033 assert_eq!(result.get_string_or("tier", "basic"), "premium");
1034 assert_eq!(result.get_i64("limit"), Some(500));
1035 assert_eq!(result.get_i64_or("limit", 100), 500);
1036 assert!(result.has_key("tier"));
1037 assert!(!result.has_key("nonexistent"));
1038 }
1039
1040 #[test]
1041 fn test_validation_result_get_value_failure() {
1042 let result = ValidationResult::failure(vec![]);
1043
1044 // All getters should return None/default for failed validation
1045 assert!(result.get_value("any").is_none());
1046 assert_eq!(result.get_string_or("any", "default"), "default");
1047 assert_eq!(result.get_i64_or("any", 42), 42);
1048 assert!(!result.has_key("any"));
1049 }
1050}