• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

getdozer / dozer / 4270349947

pending completion
4270349947

push

github

GitHub
chore: refactor grpc types (#1036)

115 of 115 new or added lines in 10 files covered. (100.0%)

27104 of 37261 relevant lines covered (72.74%)

33683.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.79
/dozer-api/src/generator/protoc/generator/implementation.rs
1
use crate::errors::GenerationError;
2
use crate::generator::protoc::generator::{
3
    CountMethodDesc, EventDesc, OnEventMethodDesc, QueryMethodDesc, RecordWithIdDesc,
4
    TokenMethodDesc, TokenResponseDesc,
5
};
6
use dozer_types::log::error;
7
use dozer_types::models::api_security::ApiSecurity;
8
use dozer_types::models::flags::Flags;
9
use dozer_types::serde::{self, Deserialize, Serialize};
10
use dozer_types::types::{FieldType, Schema};
11
use handlebars::Handlebars;
12
use inflector::Inflector;
13
use prost_reflect::{DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};
14
use std::path::{Path, PathBuf};
15

16
use super::{CountResponseDesc, QueryResponseDesc, RecordDesc, ServiceDesc};
17

18
#[derive(Debug, Clone, Serialize, Deserialize)]
23✔
19
#[serde(crate = "self::serde")]
20
struct ProtoMetadata {
21
    import_libs: Vec<String>,
22
    package_name: String,
23
    lower_name: String,
24
    plural_pascal_name: String,
25
    pascal_name: String,
26
    props: Vec<String>,
27
    version_field_id: usize,
28
    enable_token: bool,
29
    enable_on_event: bool,
30
}
31

32
pub struct ProtoGeneratorImpl<'a> {
33
    handlebars: Handlebars<'a>,
34
    schema: &'a dozer_types::types::Schema,
35
    names: Names,
36
    folder_path: &'a Path,
37
    security: &'a Option<ApiSecurity>,
38
    flags: &'a Option<Flags>,
39
}
40

41
impl<'a> ProtoGeneratorImpl<'a> {
42
    pub fn new(
23✔
43
        schema_name: &str,
23✔
44
        schema: &'a Schema,
23✔
45
        folder_path: &'a Path,
23✔
46
        security: &'a Option<ApiSecurity>,
23✔
47
        flags: &'a Option<Flags>,
23✔
48
    ) -> Result<Self, GenerationError> {
23✔
49
        let names = Names::new(schema_name, schema);
23✔
50
        let mut generator = Self {
23✔
51
            handlebars: Handlebars::new(),
23✔
52
            schema,
23✔
53
            names,
23✔
54
            folder_path,
23✔
55
            security,
23✔
56
            flags,
23✔
57
        };
23✔
58
        generator.register_template()?;
23✔
59
        Ok(generator)
23✔
60
    }
23✔
61

62
    fn register_template(&mut self) -> Result<(), GenerationError> {
23✔
63
        let main_template = include_str!("template/proto.tmpl");
23✔
64
        self.handlebars
23✔
65
            .register_template_string("main", main_template)
23✔
66
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
67
        Ok(())
23✔
68
    }
23✔
69

70
    fn props(&self) -> Vec<String> {
23✔
71
        self.schema
23✔
72
            .fields
23✔
73
            .iter()
23✔
74
            .enumerate()
23✔
75
            .zip(&self.names.record_field_names)
23✔
76
            .map(|((idx, field), field_name)| -> String {
23✔
77
                let optional = if field.nullable { "optional " } else { "" };
74✔
78
                let proto_type = convert_dozer_type_to_proto_type(field.typ.to_owned()).unwrap();
74✔
79
                format!("{optional}{proto_type} {field_name} = {};", idx + 1)
74✔
80
            })
74✔
81
            .collect()
23✔
82
    }
23✔
83

84
    fn libs_by_type(&self) -> Result<Vec<String>, GenerationError> {
23✔
85
        let type_need_import_libs = ["google.protobuf.Timestamp"];
23✔
86
        let mut libs_import: Vec<String> = self
23✔
87
            .schema
23✔
88
            .fields
23✔
89
            .iter()
23✔
90
            .map(|field| convert_dozer_type_to_proto_type(field.to_owned().typ).unwrap())
75✔
91
            .filter(|proto_type| -> bool {
75✔
92
                type_need_import_libs.contains(&proto_type.to_owned().as_str())
75✔
93
            })
75✔
94
            .map(|proto_type| match proto_type.as_str() {
23✔
95
                "google.protobuf.Timestamp" => "google/protobuf/timestamp.proto".to_owned(),
2✔
96
                _ => "".to_owned(),
×
97
            })
23✔
98
            .collect();
23✔
99
        libs_import.push("types.proto".to_owned());
23✔
100
        libs_import.sort();
23✔
101
        libs_import.dedup();
23✔
102
        Ok(libs_import)
23✔
103
    }
23✔
104

105
    fn get_metadata(&self) -> Result<ProtoMetadata, GenerationError> {
23✔
106
        let import_libs: Vec<String> = self.libs_by_type()?;
23✔
107
        let metadata = ProtoMetadata {
23✔
108
            package_name: self.names.package_name.clone(),
23✔
109
            import_libs,
23✔
110
            lower_name: self.names.lower_name.clone(),
23✔
111
            plural_pascal_name: self.names.plural_pascal_name.clone(),
23✔
112
            pascal_name: self.names.pascal_name.clone(),
23✔
113
            props: self.props(),
23✔
114
            version_field_id: self.schema.fields.len() + 1,
23✔
115
            enable_token: self.security.is_some(),
23✔
116
            enable_on_event: self.flags.clone().unwrap_or_default().push_events,
23✔
117
        };
23✔
118
        Ok(metadata)
23✔
119
    }
23✔
120

121
    pub fn generate_proto(&self) -> Result<(String, PathBuf), GenerationError> {
23✔
122
        if !Path::new(&self.folder_path).exists() {
23✔
123
            return Err(GenerationError::DirPathNotExist(
×
124
                self.folder_path.to_path_buf(),
×
125
            ));
×
126
        }
23✔
127

128
        let metadata = self.get_metadata()?;
23✔
129

130
        let types_proto = include_str!("../../../../../dozer-types/protos/types.proto");
23✔
131

132
        let resource_proto = self
23✔
133
            .handlebars
23✔
134
            .render("main", &metadata)
23✔
135
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
136

137
        // Copy types proto file
138
        let mut types_file = std::fs::File::create(self.folder_path.join("types.proto"))
23✔
139
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
140

141
        let resource_path = self.folder_path.join(&self.names.proto_file_name);
23✔
142
        let mut resource_file = std::fs::File::create(resource_path.clone())
23✔
143
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
144

145
        std::io::Write::write_all(&mut types_file, types_proto.as_bytes())
23✔
146
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
147

148
        std::io::Write::write_all(&mut resource_file, resource_proto.as_bytes())
23✔
149
            .map_err(|e| GenerationError::InternalError(Box::new(e)))?;
23✔
150

151
        Ok((resource_proto, resource_path))
23✔
152
    }
23✔
153

154
    pub fn read(
16✔
155
        descriptor: &DescriptorPool,
16✔
156
        schema_name: &str,
16✔
157
    ) -> Result<ServiceDesc, GenerationError> {
16✔
158
        fn get_field(
130✔
159
            message: &MessageDescriptor,
130✔
160
            field_name: &str,
130✔
161
        ) -> Result<FieldDescriptor, GenerationError> {
130✔
162
            message
130✔
163
                .get_field_by_name(field_name)
130✔
164
                .ok_or_else(|| GenerationError::FieldNotFound {
130✔
165
                    message_name: message.name().to_string(),
×
166
                    field_name: field_name.to_string(),
×
167
                })
130✔
168
        }
130✔
169

16✔
170
        fn record_desc_from_message(
24✔
171
            message: MessageDescriptor,
24✔
172
        ) -> Result<RecordDesc, GenerationError> {
24✔
173
            let version_field = get_field(&message, "__dozer_record_version")?;
24✔
174
            Ok(RecordDesc {
24✔
175
                message,
24✔
176
                version_field,
24✔
177
            })
24✔
178
        }
24✔
179

16✔
180
        let names = Names::new(schema_name, &Schema::empty());
16✔
181
        let service_name = format!("{}.{}", &names.package_name, &names.plural_pascal_name);
16✔
182
        let service = descriptor
16✔
183
            .get_service_by_name(&service_name)
16✔
184
            .ok_or(GenerationError::ServiceNotFound(service_name))?;
16✔
185

186
        let mut count = None;
16✔
187
        let mut query = None;
16✔
188
        let mut on_event = None;
16✔
189
        let mut token = None;
16✔
190
        for method in service.methods() {
50✔
191
            match method.name() {
50✔
192
                "count" => {
50✔
193
                    let message = method.output();
16✔
194
                    let count_field = get_field(&message, "count")?;
16✔
195
                    count = Some(CountMethodDesc {
16✔
196
                        method,
16✔
197
                        response_desc: CountResponseDesc {
16✔
198
                            message,
16✔
199
                            count_field,
16✔
200
                        },
16✔
201
                    });
16✔
202
                }
203
                "query" => {
34✔
204
                    let message = method.output();
16✔
205
                    let records_field = get_field(&message, "records")?;
16✔
206
                    let records_filed_kind = records_field.kind();
16✔
207
                    let Kind::Message(record_with_id_message) = records_filed_kind else {
16✔
208
                        return Err(GenerationError::ExpectedMessageField {
×
209
                            filed_name: records_field.full_name().to_string(),
×
210
                            actual: records_filed_kind
×
211
                        });
×
212
                    };
213
                    let id_field = get_field(&record_with_id_message, "id")?;
16✔
214
                    let record_field = get_field(&record_with_id_message, "record")?;
16✔
215
                    let record_field_kind = record_field.kind();
16✔
216
                    let Kind::Message(record_message) = record_field_kind else {
16✔
217
                        return Err(GenerationError::ExpectedMessageField {
×
218
                            filed_name: record_field.full_name().to_string(),
×
219
                            actual: record_field_kind
×
220
                        });
×
221
                    };
222
                    query = Some(QueryMethodDesc {
223
                        method,
16✔
224
                        response_desc: QueryResponseDesc {
16✔
225
                            message,
16✔
226
                            records_field,
16✔
227
                            record_with_id_desc: RecordWithIdDesc {
16✔
228
                                message: record_with_id_message,
16✔
229
                                id_field,
16✔
230
                                record_field,
16✔
231
                                record_desc: record_desc_from_message(record_message)?,
16✔
232
                            },
233
                        },
234
                    });
235
                }
236
                "on_event" => {
18✔
237
                    let message = method.output();
8✔
238
                    let typ_field = get_field(&message, "typ")?;
8✔
239
                    let old_field = get_field(&message, "old")?;
8✔
240
                    let new_field = get_field(&message, "new")?;
8✔
241
                    let new_id_field = get_field(&message, "new_id")?;
8✔
242
                    let old_field_kind = old_field.kind();
8✔
243
                    let Kind::Message(record_message) = old_field_kind else {
8✔
244
                        return Err(GenerationError::ExpectedMessageField {
×
245
                            filed_name: old_field.full_name().to_string(),
×
246
                            actual: old_field_kind
×
247
                        });
×
248
                    };
249
                    on_event = Some(OnEventMethodDesc {
250
                        method,
8✔
251
                        response_desc: EventDesc {
8✔
252
                            message,
8✔
253
                            typ_field,
8✔
254
                            old_field,
8✔
255
                            new_field,
8✔
256
                            new_id_field,
8✔
257
                            record_desc: record_desc_from_message(record_message)?,
8✔
258
                        },
259
                    });
260
                }
261
                "token" => {
10✔
262
                    let message = method.output();
10✔
263
                    let token_field = get_field(&message, "token")?;
10✔
264
                    token = Some(TokenMethodDesc {
10✔
265
                        method,
10✔
266
                        response_desc: TokenResponseDesc {
10✔
267
                            message,
10✔
268
                            token_field,
10✔
269
                        },
10✔
270
                    });
10✔
271
                }
272
                _ => {
273
                    return Err(GenerationError::UnexpectedMethod(
×
274
                        method.full_name().to_string(),
×
275
                    ))
×
276
                }
277
            }
278
        }
279

280
        let Some(count) = count else {
16✔
281
            return Err(GenerationError::MissingCountMethod(service.full_name().to_string()));
×
282
        };
283
        let Some(query) = query else {
16✔
284
            return Err(GenerationError::MissingQueryMethod(service.full_name().to_string()));
×
285
        };
286

287
        Ok(ServiceDesc {
16✔
288
            service,
16✔
289
            count,
16✔
290
            query,
16✔
291
            on_event,
16✔
292
            token,
16✔
293
        })
16✔
294
    }
16✔
295
}
296

297
struct Names {
298
    proto_file_name: String,
299
    package_name: String,
300
    lower_name: String,
301
    plural_pascal_name: String,
302
    pascal_name: String,
303
    record_field_names: Vec<String>,
304
}
305

306
impl Names {
307
    fn new(schema_name: &str, schema: &Schema) -> Self {
39✔
308
        if schema_name.contains('-') {
39✔
309
            error!("Name of the endpoint should not contain `-`.");
×
310
        }
39✔
311
        let schema_name = schema_name.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
195✔
312

39✔
313
        let package_name = format!("dozer.generated.{schema_name}");
39✔
314
        let lower_name = schema_name.to_lowercase();
39✔
315
        let plural_pascal_name = schema_name.to_pascal_case().to_plural();
39✔
316
        let pascal_name = schema_name.to_pascal_case().to_singular();
39✔
317
        let record_field_names = schema
39✔
318
            .fields
39✔
319
            .iter()
39✔
320
            .map(|field| {
75✔
321
                if field.name.contains('-') {
75✔
322
                    error!("Name of the field should not contain `-`.");
×
323
                }
75✔
324
                field
75✔
325
                    .name
75✔
326
                    .replace(|c: char| !c.is_ascii_alphanumeric(), "_")
513✔
327
            })
75✔
328
            .collect::<Vec<_>>();
39✔
329
        Self {
39✔
330
            proto_file_name: format!("{lower_name}.proto"),
39✔
331
            package_name,
39✔
332
            lower_name,
39✔
333
            plural_pascal_name,
39✔
334
            pascal_name,
39✔
335
            record_field_names,
39✔
336
        }
39✔
337
    }
39✔
338
}
339

340
fn convert_dozer_type_to_proto_type(field_type: FieldType) -> Result<String, GenerationError> {
149✔
341
    match field_type {
149✔
342
        FieldType::UInt => Ok("uint64".to_owned()),
52✔
343
        FieldType::Int => Ok("int64".to_owned()),
40✔
344
        FieldType::Float => Ok("double".to_owned()),
6✔
345
        FieldType::Boolean => Ok("bool".to_owned()),
×
346
        FieldType::String => Ok("string".to_owned()),
45✔
347
        FieldType::Text => Ok("string".to_owned()),
×
348
        FieldType::Binary => Ok("bytes".to_owned()),
×
349
        FieldType::Decimal => Ok("dozer.types.RustDecimal".to_owned()),
×
350
        FieldType::Timestamp => Ok("google.protobuf.Timestamp".to_owned()),
6✔
351
        FieldType::Date => Ok("string".to_owned()),
×
352
        FieldType::Bson => Ok("bytes".to_owned()),
×
353
        FieldType::Point => Ok("dozer.types.PointType".to_owned()),
×
354
    }
355
}
149✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc