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

getdozer / dozer / 4382580286

pending completion
4382580286

push

github

GitHub
feat: Separate cache operation log environment and index environments (#1199)

1370 of 1370 new or added lines in 33 files covered. (100.0%)

28671 of 41023 relevant lines covered (69.89%)

51121.29 hits per line

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

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

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

19
const POINT_TYPE_CLASS: &str = "dozer.types.PointType";
20
const DECIMAL_TYPE_CLASS: &str = "dozer.types.RustDecimal";
21
const TIMESTAMP_TYPE_CLASS: &str = "google.protobuf.Timestamp";
22

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

37
pub struct ProtoGeneratorImpl<'a> {
38
    handlebars: Handlebars<'a>,
39
    schema: &'a dozer_types::types::Schema,
40
    names: Names,
41
    folder_path: &'a Path,
42
    security: &'a Option<ApiSecurity>,
43
    flags: &'a Option<Flags>,
44
}
45

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

67
    fn register_template(&mut self) -> Result<(), GenerationError> {
43✔
68
        let main_template = include_str!("template/proto.tmpl");
43✔
69
        self.handlebars
43✔
70
            .register_template_string("main", main_template)
43✔
71
            .map_err(|e| GenerationError::HandlebarsTemplate(Box::new(e)))?;
43✔
72
        Ok(())
43✔
73
    }
43✔
74

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

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

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

126
    pub fn generate_proto(&self) -> Result<(String, PathBuf), GenerationError> {
43✔
127
        if !Path::new(&self.folder_path).exists() {
43✔
128
            return Err(GenerationError::DirPathNotExist(
×
129
                self.folder_path.to_path_buf(),
×
130
            ));
×
131
        }
43✔
132

133
        let metadata = self.get_metadata()?;
43✔
134

135
        let types_proto = include_str!("../../../../../dozer-types/protos/types.proto");
43✔
136

137
        let resource_proto = self.handlebars.render("main", &metadata)?;
43✔
138

139
        // Copy types proto file
140
        let types_path = self.folder_path.join("types.proto");
43✔
141
        std::fs::write(&types_path, types_proto)
43✔
142
            .map_err(|e| GenerationError::FailedToWriteToFile(types_path, e))?;
43✔
143

144
        let resource_path = self.folder_path.join(&self.names.proto_file_name);
43✔
145
        std::fs::write(&resource_path, &resource_proto)
43✔
146
            .map_err(|e| GenerationError::FailedToWriteToFile(resource_path.clone(), e))?;
43✔
147

148
        Ok((resource_proto, resource_path))
43✔
149
    }
43✔
150

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

21✔
167
        let record_desc_from_message =
21✔
168
            |message: MessageDescriptor| -> Result<RecordDesc, GenerationError> {
29✔
169
                let version_field = get_field(&message, "__dozer_record_version")?;
29✔
170

171
                if let Some(point_values) = descriptor.get_message_by_name(POINT_TYPE_CLASS) {
29✔
172
                    let pv = point_values;
29✔
173
                    if let Some(decimal_values) = descriptor.get_message_by_name(DECIMAL_TYPE_CLASS)
29✔
174
                    {
175
                        let dv = decimal_values;
29✔
176
                        Ok(RecordDesc {
29✔
177
                            message,
29✔
178
                            version_field,
29✔
179
                            point_field: PointDesc {
29✔
180
                                message: pv.clone(),
29✔
181
                                x: get_field(&pv, "x")?,
29✔
182
                                y: get_field(&pv, "y")?,
29✔
183
                            },
184
                            decimal_field: DecimalDesc {
185
                                message: dv.clone(),
29✔
186
                                flags: get_field(&dv, "flags")?,
29✔
187
                                lo: get_field(&dv, "lo")?,
29✔
188
                                mid: get_field(&dv, "mid")?,
29✔
189
                                hi: get_field(&dv, "hi")?,
29✔
190
                            },
191
                        })
192
                    } else {
193
                        Err(ServiceNotFound(DECIMAL_TYPE_CLASS.to_string()))
×
194
                    }
195
                } else {
196
                    Err(ServiceNotFound(POINT_TYPE_CLASS.to_string()))
×
197
                }
198
            };
29✔
199

200
        let names = Names::new(schema_name, &Schema::empty());
21✔
201
        let service_name = format!("{}.{}", &names.package_name, &names.plural_pascal_name);
21✔
202
        let service = descriptor
21✔
203
            .get_service_by_name(&service_name)
21✔
204
            .ok_or(GenerationError::ServiceNotFound(service_name))?;
21✔
205

206
        let mut count = None;
21✔
207
        let mut query = None;
21✔
208
        let mut on_event = None;
21✔
209
        let mut token = None;
21✔
210
        for method in service.methods() {
59✔
211
            match method.name() {
59✔
212
                "count" => {
59✔
213
                    let message = method.output();
21✔
214
                    let count_field = get_field(&message, "count")?;
21✔
215
                    count = Some(CountMethodDesc {
21✔
216
                        method,
21✔
217
                        response_desc: CountResponseDesc {
21✔
218
                            message,
21✔
219
                            count_field,
21✔
220
                        },
21✔
221
                    });
21✔
222
                }
223
                "query" => {
38✔
224
                    let message = method.output();
21✔
225
                    let records_field = get_field(&message, "records")?;
21✔
226
                    let records_filed_kind = records_field.kind();
21✔
227
                    let Kind::Message(record_with_id_message) = records_filed_kind else {
21✔
228
                        return Err(GenerationError::ExpectedMessageField {
×
229
                            filed_name: records_field.full_name().to_string(),
×
230
                            actual: records_filed_kind
×
231
                        });
×
232
                    };
233
                    let id_field = get_field(&record_with_id_message, "id")?;
21✔
234
                    let record_field = get_field(&record_with_id_message, "record")?;
21✔
235
                    let record_field_kind = record_field.kind();
21✔
236
                    let Kind::Message(record_message) = record_field_kind else {
21✔
237
                        return Err(GenerationError::ExpectedMessageField {
×
238
                            filed_name: record_field.full_name().to_string(),
×
239
                            actual: record_field_kind
×
240
                        });
×
241
                    };
242
                    query = Some(QueryMethodDesc {
243
                        method,
21✔
244
                        response_desc: QueryResponseDesc {
21✔
245
                            message,
21✔
246
                            records_field,
21✔
247
                            record_with_id_desc: RecordWithIdDesc {
21✔
248
                                message: record_with_id_message,
21✔
249
                                id_field,
21✔
250
                                record_field,
21✔
251
                                record_desc: record_desc_from_message(record_message)?,
21✔
252
                            },
253
                        },
254
                    });
255
                }
256
                "on_event" => {
17✔
257
                    let message = method.output();
8✔
258
                    let typ_field = get_field(&message, "typ")?;
8✔
259
                    let old_field = get_field(&message, "old")?;
8✔
260
                    let new_field = get_field(&message, "new")?;
8✔
261
                    let new_id_field = get_field(&message, "new_id")?;
8✔
262
                    let old_field_kind = old_field.kind();
8✔
263
                    let Kind::Message(record_message) = old_field_kind else {
8✔
264
                        return Err(GenerationError::ExpectedMessageField {
×
265
                            filed_name: old_field.full_name().to_string(),
×
266
                            actual: old_field_kind
×
267
                        });
×
268
                    };
269
                    on_event = Some(OnEventMethodDesc {
270
                        method,
8✔
271
                        response_desc: EventDesc {
8✔
272
                            message,
8✔
273
                            typ_field,
8✔
274
                            old_field,
8✔
275
                            new_field,
8✔
276
                            new_id_field,
8✔
277
                            record_desc: record_desc_from_message(record_message)?,
8✔
278
                        },
279
                    });
280
                }
281
                "token" => {
9✔
282
                    let message = method.output();
9✔
283
                    let token_field = get_field(&message, "token")?;
9✔
284
                    token = Some(TokenMethodDesc {
9✔
285
                        method,
9✔
286
                        response_desc: TokenResponseDesc {
9✔
287
                            message,
9✔
288
                            token_field,
9✔
289
                        },
9✔
290
                    });
9✔
291
                }
292
                _ => {
293
                    return Err(GenerationError::UnexpectedMethod(
×
294
                        method.full_name().to_string(),
×
295
                    ))
×
296
                }
297
            }
298
        }
299

300
        let Some(count) = count else {
21✔
301
            return Err(GenerationError::MissingCountMethod(service.full_name().to_string()));
×
302
        };
303
        let Some(query) = query else {
21✔
304
            return Err(GenerationError::MissingQueryMethod(service.full_name().to_string()));
×
305
        };
306

307
        Ok(ServiceDesc {
21✔
308
            service,
21✔
309
            count,
21✔
310
            query,
21✔
311
            on_event,
21✔
312
            token,
21✔
313
        })
21✔
314
    }
21✔
315
}
316

317
struct Names {
318
    proto_file_name: String,
319
    package_name: String,
320
    lower_name: String,
321
    plural_pascal_name: String,
322
    pascal_name: String,
323
    record_field_names: Vec<String>,
324
}
325

326
impl Names {
327
    fn new(schema_name: &str, schema: &Schema) -> Self {
64✔
328
        if schema_name.contains('-') {
64✔
329
            error!("Name of the endpoint should not contain `-`.");
×
330
        }
64✔
331
        let schema_name = schema_name.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
320✔
332

64✔
333
        let package_name = format!("dozer.generated.{schema_name}");
64✔
334
        let lower_name = schema_name.to_lowercase();
64✔
335
        let plural_pascal_name = schema_name.to_pascal_case().to_plural();
64✔
336
        let pascal_name = schema_name.to_pascal_case().to_singular();
64✔
337
        let record_field_names = schema
64✔
338
            .fields
64✔
339
            .iter()
64✔
340
            .map(|field| {
135✔
341
                if field.name.contains('-') {
135✔
342
                    error!("Name of the field should not contain `-`.");
×
343
                }
135✔
344
                field
135✔
345
                    .name
135✔
346
                    .replace(|c: char| !c.is_ascii_alphanumeric(), "_")
1,273✔
347
            })
135✔
348
            .collect::<Vec<_>>();
64✔
349
        Self {
64✔
350
            proto_file_name: format!("{lower_name}.proto"),
64✔
351
            package_name,
64✔
352
            lower_name,
64✔
353
            plural_pascal_name,
64✔
354
            pascal_name,
64✔
355
            record_field_names,
64✔
356
        }
64✔
357
    }
64✔
358
}
359

360
fn convert_dozer_type_to_proto_type(field_type: FieldType) -> Result<String, GenerationError> {
270✔
361
    match field_type {
270✔
362
        FieldType::UInt => Ok("uint64".to_owned()),
12✔
363
        FieldType::Int => Ok("int64".to_owned()),
80✔
364
        FieldType::Float => Ok("double".to_owned()),
46✔
365
        FieldType::Boolean => Ok("bool".to_owned()),
×
366
        FieldType::String => Ok("string".to_owned()),
126✔
367
        FieldType::Text => Ok("string".to_owned()),
×
368
        FieldType::Binary => Ok("bytes".to_owned()),
×
369
        FieldType::Decimal => Ok(DECIMAL_TYPE_CLASS.to_owned()),
×
370
        FieldType::Timestamp => Ok(TIMESTAMP_TYPE_CLASS.to_owned()),
6✔
371
        FieldType::Date => Ok("string".to_owned()),
×
372
        FieldType::Bson => Ok("bytes".to_owned()),
×
373
        FieldType::Point => Ok(POINT_TYPE_CLASS.to_owned()),
×
374
    }
375
}
270✔
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

© 2025 Coveralls, Inc