proto_sign/
canonical.rs

1use serde::Serialize;
2
3// Note: Using BTreeSet for sorted, unique collections.
4// This requires `Ord` to be derived.
5use std::collections::{BTreeMap, BTreeSet};
6
7//==============================================================================
8// Reserved Types for Breaking Change Detection
9//==============================================================================
10
11/// Represents a range of reserved numbers (for fields or enum values).
12#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
13pub struct ReservedRange {
14    pub start: i32,
15    pub end: i32,
16}
17
18/// Represents a reserved name.
19#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
20pub struct ReservedName {
21    pub name: String,
22}
23
24//==============================================================================
25// Structs for Exact Semantic Fingerprinting
26//==============================================================================
27
28/// Represents the semantically significant content of a .proto file.
29#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
30pub struct CanonicalFile {
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub package: Option<String>,
33    #[serde(skip_serializing_if = "String::is_empty")]
34    pub syntax: String, // "proto2", "proto3", "editions"
35    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
36    pub imports: BTreeSet<String>,
37    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
38    pub messages: BTreeSet<CanonicalMessage>,
39    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
40    pub enums: BTreeSet<CanonicalEnum>,
41    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
42    pub services: BTreeSet<CanonicalService>,
43    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
44    pub extensions: BTreeSet<CanonicalExtension>, // Extension field definitions
45
46    // ========================================
47    // File Options - Complete Set for All Rules
48    // ========================================
49
50    // Language-specific package options
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub go_package: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub java_package: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub csharp_namespace: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub ruby_package: Option<String>,
59
60    // Java-specific options
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub java_multiple_files: Option<bool>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub java_outer_classname: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub java_string_check_utf8: Option<bool>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub java_generic_services: Option<bool>,
69
70    // Objective-C options
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub objc_class_prefix: Option<String>,
73
74    // PHP options
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub php_class_prefix: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub php_namespace: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub php_metadata_namespace: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub php_generic_services: Option<bool>, // Deprecated
83
84    // Swift options
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub swift_prefix: Option<String>,
87
88    // C++ options
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub cc_generic_services: Option<bool>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub cc_enable_arenas: Option<bool>,
93
94    // Python options
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub py_generic_services: Option<bool>,
97
98    // Optimization options
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub optimize_for: Option<String>, // "SPEED", "CODE_SIZE", "LITE_RUNTIME"
101}
102
103/// Represents a Protobuf message.
104#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
105pub struct CanonicalMessage {
106    pub name: String,
107    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
108    pub fields: BTreeSet<CanonicalField>,
109    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
110    pub nested_messages: BTreeSet<CanonicalMessage>,
111    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
112    pub nested_enums: BTreeSet<CanonicalEnum>,
113    #[serde(skip_serializing_if = "Vec::is_empty")]
114    pub oneofs: Vec<String>,
115    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
116    pub reserved_ranges: BTreeSet<ReservedRange>,
117    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
118    pub reserved_names: BTreeSet<ReservedName>,
119    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
120    pub extension_ranges: BTreeSet<ReservedRange>, // Extensions use same range format
121
122    // Message-level options for breaking change rules
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub message_set_wire_format: Option<bool>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub no_standard_descriptor_accessor: Option<bool>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub deprecated: Option<bool>,
129}
130
131/// Represents a field within a Protobuf message.
132/// The sort order is primarily by field number.
133#[derive(Debug, Default, Serialize, PartialEq, Eq)]
134pub struct CanonicalField {
135    pub name: String,
136    pub number: i32,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub label: Option<String>, // "optional", "required", "repeated"
139    pub type_name: String,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub oneof_index: Option<i32>,
142
143    // ========================================
144    // Field Options - Complete Set for All Rules
145    // ========================================
146
147    // Basic field attributes
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub default: Option<String>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub json_name: Option<String>,
152
153    // Type-specific options
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub jstype: Option<String>, // "JS_NORMAL", "JS_STRING", "JS_NUMBER"
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub ctype: Option<String>, // Deprecated
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub cpp_string_type: Option<String>, // Replacement for ctype
160
161    // Validation options
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub utf8_validation: Option<String>, // "VERIFY", "NONE"
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub java_utf8_validation: Option<bool>,
166
167    // Field state
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub deprecated: Option<bool>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub weak: Option<bool>,
172
173    // Generic options map for any unrecognized options
174    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
175    pub options: BTreeMap<String, String>,
176}
177
178// Custom implementation of Ord for CanonicalField to sort by `number` first.
179impl Ord for CanonicalField {
180    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
181        self.number
182            .cmp(&other.number)
183            .then_with(|| self.name.cmp(&other.name))
184    }
185}
186
187impl PartialOrd for CanonicalField {
188    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
189        Some(self.cmp(other))
190    }
191}
192
193/// Represents a Protobuf enum.
194#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
195pub struct CanonicalEnum {
196    pub name: String,
197    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
198    pub values: BTreeSet<CanonicalEnumValue>,
199    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
200    pub reserved_ranges: BTreeSet<ReservedRange>,
201    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
202    pub reserved_names: BTreeSet<ReservedName>,
203
204    // Enum-level options for breaking change rules
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub allow_alias: Option<bool>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub deprecated: Option<bool>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub closed_enum: Option<bool>, // For editions/proto3 open vs closed
211
212    // Generic options map
213    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
214    pub options: BTreeMap<String, String>,
215}
216
217/// Represents a single value within a Protobuf enum.
218/// The sort order is primarily by number.
219#[derive(Debug, Default, Serialize, PartialEq, Eq)]
220pub struct CanonicalEnumValue {
221    pub name: String,
222    pub number: i32,
223}
224
225/// Represents a protobuf extension field definition.
226/// Extensions are fields that extend existing messages.
227#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
228pub struct CanonicalExtension {
229    pub name: String,
230    pub number: i32,
231    pub extendee: String, // The message being extended (fully qualified)
232    pub type_name: String,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub label: Option<String>, // "optional", "required", "repeated"
235
236    // Extension options (similar to field options)
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub default: Option<String>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub deprecated: Option<bool>,
241}
242
243// Custom implementation of Ord for CanonicalEnumValue to sort by `number` first.
244impl Ord for CanonicalEnumValue {
245    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
246        self.number
247            .cmp(&other.number)
248            .then_with(|| self.name.cmp(&other.name))
249    }
250}
251
252impl PartialOrd for CanonicalEnumValue {
253    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
254        Some(self.cmp(other))
255    }
256}
257
258/// Represents a Protobuf service.
259#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
260pub struct CanonicalService {
261    pub name: String,
262    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
263    pub methods: BTreeSet<CanonicalMethod>,
264}
265
266/// Represents a method within a service.
267#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord)]
268pub struct CanonicalMethod {
269    pub name: String,
270    pub input_type: String,
271    pub output_type: String,
272    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
273    pub client_streaming: bool,
274    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
275    pub server_streaming: bool,
276
277    // Method options for breaking change rules
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub idempotency_level: Option<String>, // "NO_SIDE_EFFECTS", "IDEMPOTENT", "UNKNOWN"
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub deprecated: Option<bool>,
282}