1use lsp_types::{
4 CompletionItem, CompletionItemKind, Documentation, MarkupContent, MarkupKind, Position, Range,
5};
6
7use crate::macro_analyzer::SpringMacro;
8use crate::schema::SchemaProvider;
9use crate::toml_analyzer::{TomlAnalyzer, TomlDocument};
10
11#[derive(Debug, Clone)]
15pub enum CompletionContext {
16 Toml,
18 Macro,
20 Unknown,
22}
23
24pub struct CompletionEngine {
28 toml_analyzer: TomlAnalyzer,
30}
31
32impl CompletionEngine {
33 pub fn new(schema_provider: SchemaProvider) -> Self {
39 Self {
40 toml_analyzer: TomlAnalyzer::new(schema_provider),
41 }
42 }
43
44 pub fn complete(
59 &self,
60 context: CompletionContext,
61 position: Position,
62 toml_doc: Option<&TomlDocument>,
63 macro_info: Option<&SpringMacro>,
64 ) -> Vec<CompletionItem> {
65 match context {
66 CompletionContext::Toml => {
67 if let Some(doc) = toml_doc {
68 self.complete_toml(doc, position)
69 } else {
70 Vec::new()
71 }
72 }
73 CompletionContext::Macro => {
74 if let Some(macro_info) = macro_info {
75 self.complete_macro(macro_info, None)
76 } else {
77 Vec::new()
78 }
79 }
80 CompletionContext::Unknown => Vec::new(),
81 }
82 }
83
84 pub fn complete_toml_document(
101 &self,
102 doc: &TomlDocument,
103 position: Position,
104 ) -> Vec<CompletionItem> {
105 self.complete_toml(doc, position)
106 }
107
108 fn complete_toml(&self, doc: &TomlDocument, position: Position) -> Vec<CompletionItem> {
125 if self.is_prefix_position(doc, position) {
127 return self.complete_config_prefix();
128 }
129
130 if self.is_env_var_position(doc, position) {
132 return self.complete_env_var();
133 }
134
135 if let Some(section) = self.find_section_at_position(doc, position) {
137 if let Some(property_name) = self.find_property_at_position(section, position) {
139 if let Some(property_schema) = self
140 .toml_analyzer
141 .schema_provider()
142 .get_property_schema(§ion.prefix, &property_name)
143 {
144 if let crate::schema::TypeInfo::String {
146 enum_values: Some(ref values),
147 ..
148 } = property_schema.type_info
149 {
150 return self.complete_enum_values(values);
151 }
152 }
153 }
154
155 return self.complete_config_properties(section);
157 }
158
159 Vec::new()
160 }
161
162 fn is_prefix_position(&self, doc: &TomlDocument, position: Position) -> bool {
166 let lines: Vec<&str> = doc.content.lines().collect();
168
169 if position.line as usize >= lines.len() {
170 return false;
171 }
172
173 let line = lines[position.line as usize];
174 let char_pos = position.character as usize;
175
176 if char_pos > line.len() {
178 return false;
179 }
180
181 let before_cursor = if char_pos > 0 { &line[..char_pos] } else { "" };
183
184 let trimmed = before_cursor.trim_start();
187
188 trimmed.starts_with('[') && !trimmed.contains(']') && !line.contains(']')
191 }
192
193 fn is_env_var_position(&self, _doc: &TomlDocument, _position: Position) -> bool {
197 false
200 }
201
202 fn find_section_at_position<'a>(
206 &self,
207 doc: &'a TomlDocument,
208 position: Position,
209 ) -> Option<&'a crate::toml_analyzer::ConfigSection> {
210 doc.config_sections
211 .values()
212 .find(|§ion| self.position_in_range(position, section.range))
213 }
214
215 fn find_property_at_position(
219 &self,
220 section: &crate::toml_analyzer::ConfigSection,
221 position: Position,
222 ) -> Option<String> {
223 for (key, property) in §ion.properties {
224 if self.position_in_range(position, property.range) {
226 return Some(key.clone());
227 }
228 }
229 None
230 }
231
232 fn position_in_range(&self, position: Position, range: Range) -> bool {
234 if position.line < range.start.line || position.line > range.end.line {
235 return false;
236 }
237 if position.line == range.start.line && position.character < range.start.character {
238 return false;
239 }
240 if position.line == range.end.line && position.character > range.end.character {
241 return false;
242 }
243 true
244 }
245
246 fn complete_config_prefix(&self) -> Vec<CompletionItem> {
250 let prefixes = self.toml_analyzer.schema_provider().get_all_prefixes();
251
252 prefixes
253 .into_iter()
254 .map(|prefix: String| {
255 let plugin_schema = self
256 .toml_analyzer
257 .schema_provider()
258 .get_plugin_schema(&prefix);
259 let description = plugin_schema
260 .as_ref()
261 .and_then(|s| s.properties.values().next())
262 .map(|p| p.description.clone())
263 .unwrap_or_else(|| format!("{} 插件配置", prefix));
264
265 CompletionItem {
266 label: prefix.clone(),
267 kind: Some(CompletionItemKind::MODULE),
268 detail: Some(format!("[{}] 配置节", prefix)),
269 documentation: Some(Documentation::MarkupContent(MarkupContent {
270 kind: MarkupKind::Markdown,
271 value: format!(
272 "**{}** 插件配置节\n\n{}\n\n\
273 **使用方式**:\n\
274 ```toml\n\
275 [{}]\n\
276 # 配置项...\n\
277 ```",
278 prefix, description, prefix
279 ),
280 })),
281 insert_text: Some(format!("[{}]\n", prefix)),
282 insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
283 ..Default::default()
284 }
285 })
286 .collect()
287 }
288
289 fn complete_config_properties(
293 &self,
294 section: &crate::toml_analyzer::ConfigSection,
295 ) -> Vec<CompletionItem> {
296 let plugin_schema = match self
297 .toml_analyzer
298 .schema_provider()
299 .get_plugin_schema(§ion.prefix)
300 {
301 Some(schema) => schema,
302 None => return Vec::new(),
303 };
304
305 let mut completions = Vec::new();
306
307 for (key, property_schema) in &plugin_schema.properties {
308 if section.properties.contains_key(key.as_str()) {
310 continue;
311 }
312
313 let type_hint = self.type_info_to_hint(&property_schema.type_info);
314 let default_value = property_schema
315 .default
316 .as_ref()
317 .map(|v| self.value_to_string(v))
318 .unwrap_or_else(|| self.type_info_to_default(&property_schema.type_info));
319
320 let insert_text = format!("{} = {} # {}", key, default_value, type_hint);
321
322 completions.push(CompletionItem {
323 label: key.clone(),
324 kind: Some(CompletionItemKind::PROPERTY),
325 detail: Some(format!("{} ({})", property_schema.description, type_hint)),
326 documentation: Some(Documentation::MarkupContent(MarkupContent {
327 kind: MarkupKind::Markdown,
328 value: format!(
329 "**{}**\n\n{}\n\n\
330 **类型**: {}\n\
331 **默认值**: {}\n\
332 {}",
333 key,
334 property_schema.description,
335 type_hint,
336 property_schema
337 .default
338 .as_ref()
339 .map(|v| self.value_to_string(v))
340 .unwrap_or_else(|| "无".to_string()),
341 if property_schema.required {
342 "**必需**: 是"
343 } else {
344 ""
345 }
346 ),
347 })),
348 insert_text: Some(insert_text),
349 insert_text_format: Some(lsp_types::InsertTextFormat::PLAIN_TEXT),
350 ..Default::default()
351 });
352 }
353
354 completions
355 }
356
357 fn complete_enum_values(&self, values: &[String]) -> Vec<CompletionItem> {
361 values
362 .iter()
363 .map(|value| CompletionItem {
364 label: value.clone(),
365 kind: Some(CompletionItemKind::ENUM_MEMBER),
366 detail: Some(format!("枚举值: {}", value)),
367 documentation: Some(Documentation::MarkupContent(MarkupContent {
368 kind: MarkupKind::Markdown,
369 value: format!("枚举值 `{}`", value),
370 })),
371 insert_text: Some(format!("\"{}\"", value)),
372 insert_text_format: Some(lsp_types::InsertTextFormat::PLAIN_TEXT),
373 ..Default::default()
374 })
375 .collect()
376 }
377
378 pub fn complete_env_var(&self) -> Vec<CompletionItem> {
382 let common_vars = vec![
383 ("HOST", "主机地址"),
384 ("PORT", "端口号"),
385 ("DATABASE_URL", "数据库连接 URL"),
386 ("REDIS_URL", "Redis 连接 URL"),
387 ("LOG_LEVEL", "日志级别"),
388 ("ENV", "运行环境"),
389 ("DEBUG", "调试模式"),
390 ];
391
392 common_vars
393 .into_iter()
394 .map(|(name, description)| CompletionItem {
395 label: name.to_string(),
396 kind: Some(CompletionItemKind::VARIABLE),
397 detail: Some(description.to_string()),
398 documentation: Some(Documentation::MarkupContent(MarkupContent {
399 kind: MarkupKind::Markdown,
400 value: format!(
401 "**{}**\n\n{}\n\n\
402 **使用方式**:\n\
403 ```toml\n\
404 value = \"${{{}:default}}\"\n\
405 ```",
406 name, description, name
407 ),
408 })),
409 insert_text: Some(format!("{}:${{1:default}}}}", name)),
410 insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
411 ..Default::default()
412 })
413 .collect()
414 }
415
416 fn type_info_to_hint(&self, type_info: &crate::schema::TypeInfo) -> String {
418 match type_info {
419 crate::schema::TypeInfo::String {
420 enum_values: Some(values),
421 ..
422 } => {
423 format!("enum: {:?}", values)
424 }
425 crate::schema::TypeInfo::String { .. } => "string".to_string(),
426 crate::schema::TypeInfo::Integer { min, max } => {
427 if let (Some(min), Some(max)) = (min, max) {
428 format!("integer ({} - {})", min, max)
429 } else {
430 "integer".to_string()
431 }
432 }
433 crate::schema::TypeInfo::Float { .. } => "float".to_string(),
434 crate::schema::TypeInfo::Boolean => "boolean".to_string(),
435 crate::schema::TypeInfo::Array { .. } => "array".to_string(),
436 crate::schema::TypeInfo::Object { .. } => "object".to_string(),
437 }
438 }
439
440 fn type_info_to_default(&self, type_info: &crate::schema::TypeInfo) -> String {
442 match type_info {
443 crate::schema::TypeInfo::String {
444 enum_values: Some(values),
445 ..
446 } => {
447 if let Some(first) = values.first() {
448 format!("\"{}\"", first)
449 } else {
450 "\"\"".to_string()
451 }
452 }
453 crate::schema::TypeInfo::String { .. } => "\"\"".to_string(),
454 crate::schema::TypeInfo::Integer { .. } => "0".to_string(),
455 crate::schema::TypeInfo::Float { .. } => "0.0".to_string(),
456 crate::schema::TypeInfo::Boolean => "false".to_string(),
457 crate::schema::TypeInfo::Array { .. } => "[]".to_string(),
458 crate::schema::TypeInfo::Object { .. } => "{}".to_string(),
459 }
460 }
461
462 fn value_to_string(&self, value: &crate::schema::Value) -> String {
464 match value {
465 crate::schema::Value::String(s) => format!("\"{}\"", s),
466 crate::schema::Value::Integer(i) => i.to_string(),
467 crate::schema::Value::Float(f) => f.to_string(),
468 crate::schema::Value::Boolean(b) => b.to_string(),
469 crate::schema::Value::Array(_) => "[]".to_string(),
470 crate::schema::Value::Table(_) => "{}".to_string(),
471 }
472 }
473
474 pub fn complete_macro(
487 &self,
488 macro_info: &SpringMacro,
489 _cursor_position: Option<&str>,
490 ) -> Vec<CompletionItem> {
491 match macro_info {
492 SpringMacro::DeriveService(_) => self.complete_service_macro(),
493 SpringMacro::Inject(_) => self.complete_inject_macro(),
494 SpringMacro::AutoConfig(_) => self.complete_auto_config_macro(),
495 SpringMacro::Route(_) => self.complete_route_macro(),
496 SpringMacro::Job(_) => self.complete_job_macro(),
497 }
498 }
499
500 fn complete_service_macro(&self) -> Vec<CompletionItem> {
504 vec![
505 CompletionItem {
507 label: "inject(component)".to_string(),
508 kind: Some(CompletionItemKind::PROPERTY),
509 detail: Some("注入组件".to_string()),
510 documentation: Some(Documentation::MarkupContent(MarkupContent {
511 kind: MarkupKind::Markdown,
512 value: "从应用上下文中注入已注册的组件实例。\n\n\
513 **示例**:\n\
514 ```rust\n\
515 #[inject(component)]\n\
516 db: ConnectPool,\n\
517 ```"
518 .to_string(),
519 })),
520 insert_text: Some("inject(component)".to_string()),
521 ..Default::default()
522 },
523 CompletionItem {
525 label: "inject(component = \"name\")".to_string(),
526 kind: Some(CompletionItemKind::PROPERTY),
527 detail: Some("注入指定名称的组件".to_string()),
528 documentation: Some(Documentation::MarkupContent(MarkupContent {
529 kind: MarkupKind::Markdown,
530 value: "使用指定名称从应用上下文中注入组件,适用于多实例场景(如多数据源)。\n\n\
531 **示例**:\n\
532 ```rust\n\
533 #[inject(component = \"primary\")]\n\
534 primary_db: ConnectPool,\n\
535 ```"
536 .to_string(),
537 })),
538 insert_text: Some("inject(component = \"$1\")".to_string()),
539 insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
540 ..Default::default()
541 },
542 CompletionItem {
544 label: "inject(config)".to_string(),
545 kind: Some(CompletionItemKind::PROPERTY),
546 detail: Some("注入配置".to_string()),
547 documentation: Some(Documentation::MarkupContent(MarkupContent {
548 kind: MarkupKind::Markdown,
549 value: "从配置文件中加载配置项。配置项通过 `#[config_prefix]` 指定的前缀从 `config/app.toml` 中读取。\n\n\
550 **示例**:\n\
551 ```rust\n\
552 #[inject(config)]\n\
553 config: MyConfig,\n\
554 ```"
555 .to_string(),
556 })),
557 insert_text: Some("inject(config)".to_string()),
558 ..Default::default()
559 },
560 ]
561 }
562
563 fn complete_inject_macro(&self) -> Vec<CompletionItem> {
567 vec![
568 CompletionItem {
570 label: "component".to_string(),
571 kind: Some(CompletionItemKind::KEYWORD),
572 detail: Some("注入组件".to_string()),
573 documentation: Some(Documentation::MarkupContent(MarkupContent {
574 kind: MarkupKind::Markdown,
575 value: "从应用上下文中注入已注册的组件实例。\n\n\
576 **使用方式**:\n\
577 - `#[inject(component)]` - 按类型自动查找\n\
578 - `#[inject(component = \"name\")]` - 按名称查找"
579 .to_string(),
580 })),
581 insert_text: Some("component".to_string()),
582 ..Default::default()
583 },
584 CompletionItem {
586 label: "config".to_string(),
587 kind: Some(CompletionItemKind::KEYWORD),
588 detail: Some("注入配置".to_string()),
589 documentation: Some(Documentation::MarkupContent(MarkupContent {
590 kind: MarkupKind::Markdown,
591 value: "从配置文件中加载配置项。\n\n\
592 **使用方式**:\n\
593 - `#[inject(config)]` - 从 config/app.toml 加载配置"
594 .to_string(),
595 })),
596 insert_text: Some("config".to_string()),
597 ..Default::default()
598 },
599 ]
600 }
601
602 fn complete_auto_config_macro(&self) -> Vec<CompletionItem> {
606 vec![
607 CompletionItem {
608 label: "WebConfigurator".to_string(),
609 kind: Some(CompletionItemKind::CLASS),
610 detail: Some("Web 路由配置器".to_string()),
611 documentation: Some(Documentation::MarkupContent(MarkupContent {
612 kind: MarkupKind::Markdown,
613 value: "自动注册 Web 路由处理器。\n\n\
614 **示例**:\n\
615 ```rust\n\
616 #[auto_config(WebConfigurator)]\n\
617 #[tokio::main]\n\
618 async fn main() {\n\
619 App::new().add_plugin(WebPlugin).run().await\n\
620 }\n\
621 ```"
622 .to_string(),
623 })),
624 insert_text: Some("WebConfigurator".to_string()),
625 ..Default::default()
626 },
627 CompletionItem {
628 label: "JobConfigurator".to_string(),
629 kind: Some(CompletionItemKind::CLASS),
630 detail: Some("任务调度配置器".to_string()),
631 documentation: Some(Documentation::MarkupContent(MarkupContent {
632 kind: MarkupKind::Markdown,
633 value: "自动注册定时任务。\n\n\
634 **示例**:\n\
635 ```rust\n\
636 #[auto_config(JobConfigurator)]\n\
637 #[tokio::main]\n\
638 async fn main() {\n\
639 App::new().add_plugin(JobPlugin).run().await\n\
640 }\n\
641 ```"
642 .to_string(),
643 })),
644 insert_text: Some("JobConfigurator".to_string()),
645 ..Default::default()
646 },
647 CompletionItem {
648 label: "StreamConfigurator".to_string(),
649 kind: Some(CompletionItemKind::CLASS),
650 detail: Some("流处理配置器".to_string()),
651 documentation: Some(Documentation::MarkupContent(MarkupContent {
652 kind: MarkupKind::Markdown,
653 value: "自动注册流消费者。\n\n\
654 **示例**:\n\
655 ```rust\n\
656 #[auto_config(StreamConfigurator)]\n\
657 #[tokio::main]\n\
658 async fn main() {\n\
659 App::new().add_plugin(StreamPlugin).run().await\n\
660 }\n\
661 ```"
662 .to_string(),
663 })),
664 insert_text: Some("StreamConfigurator".to_string()),
665 ..Default::default()
666 },
667 ]
668 }
669
670 fn complete_route_macro(&self) -> Vec<CompletionItem> {
674 let mut completions = Vec::new();
675
676 let methods = vec![
678 ("GET", "获取资源"),
679 ("POST", "创建资源"),
680 ("PUT", "更新资源(完整)"),
681 ("DELETE", "删除资源"),
682 ("PATCH", "更新资源(部分)"),
683 ("HEAD", "获取资源头信息"),
684 ("OPTIONS", "获取支持的方法"),
685 ];
686
687 for (method, description) in methods {
688 completions.push(CompletionItem {
689 label: method.to_string(),
690 kind: Some(CompletionItemKind::CONSTANT),
691 detail: Some(description.to_string()),
692 documentation: Some(Documentation::MarkupContent(MarkupContent {
693 kind: MarkupKind::Markdown,
694 value: format!(
695 "HTTP {} 方法\n\n\
696 **示例**:\n\
697 ```rust\n\
698 #[{}(\"/path\")]\n\
699 async fn handler() -> impl IntoResponse {{\n\
700 }}\n\
701 ```",
702 method,
703 method.to_lowercase()
704 ),
705 })),
706 insert_text: Some(method.to_string()),
707 ..Default::default()
708 });
709 }
710
711 completions.push(CompletionItem {
713 label: "{id}".to_string(),
714 kind: Some(CompletionItemKind::SNIPPET),
715 detail: Some("路径参数".to_string()),
716 documentation: Some(Documentation::MarkupContent(MarkupContent {
717 kind: MarkupKind::Markdown,
718 value: "路径参数占位符,用于捕获 URL 中的动态部分。\n\n\
719 **示例**:\n\
720 ```rust\n\
721 #[get(\"/users/{id}\")]\n\
722 async fn get_user(Path(id): Path<i64>) -> impl IntoResponse {\n\
723 }\n\
724 ```"
725 .to_string(),
726 })),
727 insert_text: Some("{${1:id}}".to_string()),
728 insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
729 ..Default::default()
730 });
731
732 completions
733 }
734
735 fn complete_job_macro(&self) -> Vec<CompletionItem> {
739 vec![
740 CompletionItem {
742 label: "0 0 * * * *".to_string(),
743 kind: Some(CompletionItemKind::SNIPPET),
744 detail: Some("每小时执行".to_string()),
745 documentation: Some(Documentation::MarkupContent(MarkupContent {
746 kind: MarkupKind::Markdown,
747 value: "Cron 表达式:每小时的第 0 分 0 秒执行\n\n\
748 **格式**: 秒 分 时 日 月 星期\n\n\
749 **示例**:\n\
750 ```rust\n\
751 #[cron(\"0 0 * * * *\")]\n\
752 async fn hourly_job() {\n\
753 }\n\
754 ```"
755 .to_string(),
756 })),
757 insert_text: Some("\"0 0 * * * *\"".to_string()),
758 ..Default::default()
759 },
760 CompletionItem {
761 label: "0 0 0 * * *".to_string(),
762 kind: Some(CompletionItemKind::SNIPPET),
763 detail: Some("每天午夜执行".to_string()),
764 documentation: Some(Documentation::MarkupContent(MarkupContent {
765 kind: MarkupKind::Markdown,
766 value: "Cron 表达式:每天午夜 00:00:00 执行\n\n\
767 **格式**: 秒 分 时 日 月 星期\n\n\
768 **示例**:\n\
769 ```rust\n\
770 #[cron(\"0 0 0 * * *\")]\n\
771 async fn daily_job() {\n\
772 }\n\
773 ```"
774 .to_string(),
775 })),
776 insert_text: Some("\"0 0 0 * * *\"".to_string()),
777 ..Default::default()
778 },
779 CompletionItem {
780 label: "0 */5 * * * *".to_string(),
781 kind: Some(CompletionItemKind::SNIPPET),
782 detail: Some("每 5 分钟执行".to_string()),
783 documentation: Some(Documentation::MarkupContent(MarkupContent {
784 kind: MarkupKind::Markdown,
785 value: "Cron 表达式:每 5 分钟执行一次\n\n\
786 **格式**: 秒 分 时 日 月 星期\n\n\
787 **示例**:\n\
788 ```rust\n\
789 #[cron(\"0 */5 * * * *\")]\n\
790 async fn every_five_minutes() {\n\
791 }\n\
792 ```"
793 .to_string(),
794 })),
795 insert_text: Some("\"0 */5 * * * *\"".to_string()),
796 ..Default::default()
797 },
798 CompletionItem {
800 label: "5".to_string(),
801 kind: Some(CompletionItemKind::VALUE),
802 detail: Some("延迟 5 秒".to_string()),
803 documentation: Some(Documentation::MarkupContent(MarkupContent {
804 kind: MarkupKind::Markdown,
805 value: "任务完成后延迟 5 秒再次执行\n\n\
806 **示例**:\n\
807 ```rust\n\
808 #[fix_delay(5)]\n\
809 async fn delayed_job() {\n\
810 }\n\
811 ```"
812 .to_string(),
813 })),
814 insert_text: Some("5".to_string()),
815 ..Default::default()
816 },
817 CompletionItem {
818 label: "10".to_string(),
819 kind: Some(CompletionItemKind::VALUE),
820 detail: Some("延迟/频率 10 秒".to_string()),
821 documentation: Some(Documentation::MarkupContent(MarkupContent {
822 kind: MarkupKind::Markdown,
823 value: "延迟或频率为 10 秒\n\n\
824 **fix_delay 示例**:\n\
825 ```rust\n\
826 #[fix_delay(10)]\n\
827 async fn delayed_job() {\n\
828 }\n\
829 ```\n\n\
830 **fix_rate 示例**:\n\
831 ```rust\n\
832 #[fix_rate(10)]\n\
833 async fn periodic_job() {\n\
834 }\n\
835 ```"
836 .to_string(),
837 })),
838 insert_text: Some("10".to_string()),
839 ..Default::default()
840 },
841 CompletionItem {
842 label: "60".to_string(),
843 kind: Some(CompletionItemKind::VALUE),
844 detail: Some("延迟/频率 60 秒(1 分钟)".to_string()),
845 documentation: Some(Documentation::MarkupContent(MarkupContent {
846 kind: MarkupKind::Markdown,
847 value: "延迟或频率为 60 秒(1 分钟)\n\n\
848 **fix_delay 示例**:\n\
849 ```rust\n\
850 #[fix_delay(60)]\n\
851 async fn delayed_job() {\n\
852 }\n\
853 ```\n\n\
854 **fix_rate 示例**:\n\
855 ```rust\n\
856 #[fix_rate(60)]\n\
857 async fn periodic_job() {\n\
858 }\n\
859 ```"
860 .to_string(),
861 })),
862 insert_text: Some("60".to_string()),
863 ..Default::default()
864 },
865 ]
866 }
867}
868
869impl Default for CompletionEngine {
870 fn default() -> Self {
871 Self::new(SchemaProvider::default())
872 }
873}
874
875#[cfg(test)]
876mod tests;