1use std::collections::HashMap;
2
3use crate::ast::{ConfigBlock, Document, Value};
4use crate::diag::Diagnostic;
5use crate::dict::Compound;
6use crate::span::Span;
7use crate::template::Template;
8
9const DEFAULT_NAMING: &[(&str, &str)] = &[
11 ("primary_key", "id"),
12 ("foreign_key", "${singular(table)}_id"),
13 ("index", "idx_${table}_${columns}"),
14 ("unique_index", "uq_${table}_${columns}"),
15 ("column_separator", "_"),
16 ("noun_separator", "_"),
17 ("belongs_to", "${singular(table)}"),
18 ("has_many", "${plural(table)}"),
19 ("has_one", "${singular(table)}"),
20];
21
22const NAMING_KEYS: &[&str] = &[
23 "table_name",
24 "primary_key",
25 "foreign_key",
26 "index",
27 "unique_index",
28 "column_separator",
29 "noun_separator",
30 "belongs_to",
31 "has_many",
32 "has_one",
33];
34
35const CONSTRAINT_KEYS: &[&str] = &[
36 "null_default",
37 "on_delete_default",
38 "on_update_default",
39 "foreign_key_index",
40];
41
42const REFERENTIAL_ACTIONS: &[&str] = &["cascade", "restrict", "set_null", "no_action"];
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum TableNameStyle {
46 Plural,
47 Singular,
48}
49
50#[derive(Debug, Clone)]
51pub struct Naming {
52 pub table_name: TableNameStyle,
53 pub primary_key: String,
54 pub foreign_key: Template,
55 pub index: Template,
56 pub unique_index: Template,
57 pub column_separator: String,
58 pub noun_separator: String,
59 pub belongs_to: Template,
60 pub has_many: Template,
61 pub has_one: Template,
62}
63
64#[derive(Debug, Clone)]
65pub struct Constraints {
66 pub null_default: bool,
67 pub on_delete_default: String,
68 pub on_update_default: String,
69 pub foreign_key_index: bool,
70}
71
72#[derive(Debug, Clone)]
73pub struct Config {
74 pub naming: Naming,
75 pub constraints: Constraints,
76}
77
78impl Default for Config {
79 fn default() -> Self {
80 let t = |key: &str| {
81 let src = DEFAULT_NAMING
82 .iter()
83 .find(|(k, _)| *k == key)
84 .map(|(_, v)| *v)
85 .unwrap_or_default();
86 Template::parse(src).unwrap_or(Template { segments: vec![] })
87 };
88 Config {
89 naming: Naming {
90 table_name: TableNameStyle::Singular,
91 primary_key: "id".into(),
92 foreign_key: t("foreign_key"),
93 index: t("index"),
94 unique_index: t("unique_index"),
95 column_separator: "_".into(),
96 noun_separator: "_".into(),
97 belongs_to: t("belongs_to"),
98 has_many: t("has_many"),
99 has_one: t("has_one"),
100 },
101 constraints: Constraints {
102 null_default: false,
103 on_delete_default: "cascade".into(),
104 on_update_default: "cascade".into(),
105 foreign_key_index: true,
106 },
107 }
108 }
109}
110
111impl Config {
112 pub fn from_document(doc: &Document, diags: &mut Vec<Diagnostic>) -> Config {
113 let mut config = Config::default();
114 if let Some(block) = &doc.naming {
115 config.apply_naming(block, diags);
116 }
117 if let Some(block) = &doc.constraints {
118 config.apply_constraints(block, diags);
119 }
120 config
121 }
122
123 fn apply_naming(&mut self, block: &ConfigBlock, diags: &mut Vec<Diagnostic>) {
124 for entry in &block.entries {
125 let key = entry.key.value.as_str();
126 if !NAMING_KEYS.contains(&key) {
127 diags.push(Diagnostic::error(
128 entry.key.span,
129 format!("知らない `naming` のキー `{key}`"),
130 ));
131 continue;
132 }
133 let span = entry.value.span;
134 match key {
135 "table_name" => match value_ident(&entry.value.value) {
136 Some("plural") => self.naming.table_name = TableNameStyle::Plural,
137 Some("singular") => self.naming.table_name = TableNameStyle::Singular,
138 _ => diags.push(Diagnostic::error(
139 span,
140 "`table_name` は `plural` か `singular`",
141 )),
142 },
143 "primary_key" | "column_separator" | "noun_separator" => {
144 match value_string(&entry.value.value) {
145 Some(s) => match key {
146 "primary_key" => self.naming.primary_key = s.into(),
147 "column_separator" => self.naming.column_separator = s.into(),
148 _ => self.naming.noun_separator = s.into(),
149 },
150 None => {
151 diags.push(Diagnostic::error(span, format!("`{key}` には文字列を書く")))
152 }
153 }
154 }
155 _ => {
156 let Some(s) = value_string(&entry.value.value) else {
157 diags.push(Diagnostic::error(span, format!("`{key}` には文字列を書く")));
158 continue;
159 };
160 match Template::parse(s) {
161 Ok(t) => match key {
162 "foreign_key" => self.naming.foreign_key = t,
163 "index" => self.naming.index = t,
164 "unique_index" => self.naming.unique_index = t,
165 "belongs_to" => self.naming.belongs_to = t,
166 "has_many" => self.naming.has_many = t,
167 "has_one" => self.naming.has_one = t,
168 _ => {}
169 },
170 Err(msg) => diags.push(Diagnostic::error(span, msg)),
171 }
172 }
173 }
174 }
175 }
176
177 fn apply_constraints(&mut self, block: &ConfigBlock, diags: &mut Vec<Diagnostic>) {
178 for entry in &block.entries {
179 let key = entry.key.value.as_str();
180 if !CONSTRAINT_KEYS.contains(&key) {
181 diags.push(Diagnostic::error(
182 entry.key.span,
183 format!("知らない `constraints` のキー `{key}`"),
184 ));
185 continue;
186 }
187 let span = entry.value.span;
188 match key {
189 "null_default" | "foreign_key_index" => match value_bool(&entry.value.value) {
190 Some(b) => {
191 if key == "null_default" {
192 self.constraints.null_default = b;
193 } else {
194 self.constraints.foreign_key_index = b;
195 }
196 }
197 None => diags.push(Diagnostic::error(
198 span,
199 format!("`{key}` は `true` か `false`"),
200 )),
201 },
202 _ => match value_ident(&entry.value.value) {
203 Some(a) if REFERENTIAL_ACTIONS.contains(&a) => {
204 if key == "on_delete_default" {
205 self.constraints.on_delete_default = a.into();
206 } else {
207 self.constraints.on_update_default = a.into();
208 }
209 }
210 _ => diags.push(Diagnostic::error(
211 span,
212 format!("`{key}` は {} のいずれか", REFERENTIAL_ACTIONS.join(" / ")),
213 )),
214 },
215 }
216 }
217 }
218}
219
220fn value_ident(v: &Value) -> Option<&str> {
221 match v {
222 Value::Ident(s) => Some(s),
223 _ => None,
224 }
225}
226
227fn value_string(v: &Value) -> Option<&str> {
228 match v {
229 Value::Str(s) => Some(s),
230 _ => None,
231 }
232}
233
234fn value_bool(v: &Value) -> Option<bool> {
235 match value_ident(v)? {
236 "true" => Some(true),
237 "false" => Some(false),
238 _ => None,
239 }
240}
241
242pub type Vars = HashMap<&'static str, Compound>;
244
245pub fn missing_var(span: Span, name: &str) -> Diagnostic {
246 Diagnostic::error(
247 span,
248 format!("テンプレート変数 `{name}` はここでは使えない"),
249 )
250}