enm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/Cargo.lock +1030 -0
- data/Cargo.toml +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +29 -0
- data/ext/enm/Cargo.toml +22 -0
- data/ext/enm/build.rs +5 -0
- data/ext/enm/extconf.rb +6 -0
- data/ext/enm/src/lib.rs +543 -0
- data/lib/enm/database.rb +12 -0
- data/lib/enm/enum.rb +90 -0
- data/lib/enm/namespace.rb +176 -0
- data/lib/enm/types.rb +28 -0
- data/lib/enm/version.rb +5 -0
- data/lib/enm.rb +106 -0
- data/mise.toml +2 -0
- metadata +106 -0
data/ext/enm/src/lib.rs
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
use magnus::{
|
|
2
|
+
block::Yield, function, method, prelude::*, Error, ExceptionClass, RModule, Ruby, TryConvert,
|
|
3
|
+
Value,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use pgenum_parser::{Database, EnumType, PGEnumError, RON_VERSION};
|
|
7
|
+
|
|
8
|
+
/// Convert an `anyhow::Error` into a `magnus::Error` with the appropriate
|
|
9
|
+
/// Ruby exception class based on the `PGEnumError` variant.
|
|
10
|
+
fn to_ruby_err(err: anyhow::Error) -> Error {
|
|
11
|
+
let ruby = Ruby::get().expect("called outside Ruby");
|
|
12
|
+
let module: RModule = ruby.class_object().const_get("ENM").expect("ENM module not defined");
|
|
13
|
+
|
|
14
|
+
let (class_name, message) = match err.downcast_ref::<PGEnumError>() {
|
|
15
|
+
Some(PGEnumError::DuplicatedType { .. }) => ("DuplicatedTypeError", err.to_string()),
|
|
16
|
+
Some(PGEnumError::DuplicatedValue { .. }) => ("DuplicatedValueError", err.to_string()),
|
|
17
|
+
Some(PGEnumError::EnumConflict { .. }) => ("EnumConflictError", err.to_string()),
|
|
18
|
+
Some(PGEnumError::EnumNotFound { .. }) => ("EnumNotFoundError", err.to_string()),
|
|
19
|
+
Some(PGEnumError::InvalidRepresentation(_)) => ("SerializationError", err.to_string()),
|
|
20
|
+
Some(PGEnumError::IOError { .. }) => ("IOError", err.to_string()),
|
|
21
|
+
Some(PGEnumError::SQLParseError(_)) => ("ParseError", err.to_string()),
|
|
22
|
+
Some(PGEnumError::UnknownVersion(_)) => ("UnknownVersionError", err.to_string()),
|
|
23
|
+
None => ("Error", err.to_string()),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let exception_class: ExceptionClass = module
|
|
27
|
+
.const_get(class_name)
|
|
28
|
+
.expect("exception class not defined");
|
|
29
|
+
|
|
30
|
+
Error::new(exception_class, message)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A wrapper around `pgenum_parser::Database`. It provides
|
|
34
|
+
/// ruby bindings to the underlying Rust implementation.
|
|
35
|
+
#[magnus::wrap(class = "ENM::Database", free_immediately, size)]
|
|
36
|
+
struct DatabaseWrapper {
|
|
37
|
+
inner: Database,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl DatabaseWrapper {
|
|
41
|
+
fn new(inner: Database) -> Self {
|
|
42
|
+
Self { inner }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn empty(&self) -> bool {
|
|
46
|
+
self.inner.enums().is_empty()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn from_ron(version: u32, ron: String) -> Result<Self, Error> {
|
|
50
|
+
let inner = Database::from_ron(version, &ron).map_err(to_ruby_err)?;
|
|
51
|
+
|
|
52
|
+
Ok(Self::new(inner))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn value_to_string(value: Value) -> Result<String, Error> {
|
|
56
|
+
String::try_convert(value).or_else(|_| value.funcall("to_s", ()))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn from_ron_file(version: u32, path: Value) -> Result<Self, Error> {
|
|
60
|
+
let path = Self::value_to_string(path)?;
|
|
61
|
+
let inner = Database::from_ron_file(version, &path).map_err(to_ruby_err)?;
|
|
62
|
+
Ok(Self::new(inner))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn from_sql(name: String, sql: String) -> Result<Self, Error> {
|
|
66
|
+
let inner = Database::from_sql(&name, &sql).map_err(to_ruby_err)?;
|
|
67
|
+
|
|
68
|
+
Ok(Self::new(inner))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn from_sql_file(name: String, path: Value) -> Result<Self, Error> {
|
|
72
|
+
let path = Self::value_to_string(path)?;
|
|
73
|
+
let inner = Database::from_sql_file(&name, &path).map_err(to_ruby_err)?;
|
|
74
|
+
|
|
75
|
+
Ok(Self::new(inner))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn get(&self, name: String, schema: Option<String>) -> Result<Enum, Error> {
|
|
79
|
+
let enum_type = self
|
|
80
|
+
.inner
|
|
81
|
+
.get(schema.as_deref(), &name)
|
|
82
|
+
.map_err(to_ruby_err)?;
|
|
83
|
+
|
|
84
|
+
Ok(Enum {
|
|
85
|
+
inner: enum_type.clone(),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn lazy_get(&self, key: Value) -> Result<Enum, Error> {
|
|
90
|
+
let s: String = String::try_convert(key)
|
|
91
|
+
.or_else(|_| key.funcall("to_s", ()))?;
|
|
92
|
+
|
|
93
|
+
if let Some((schema, name)) = s.split_once('.') {
|
|
94
|
+
self.get(name.to_string(), Some(schema.to_string()))
|
|
95
|
+
} else {
|
|
96
|
+
self.get(s, None)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn name(&self) -> String {
|
|
101
|
+
self.inner.name().to_string()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn to_ron(&self) -> Result<String, Error> {
|
|
105
|
+
self.inner.to_ron().map_err(to_ruby_err)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn to_ron_file(&self, path: Value) -> Result<(), Error> {
|
|
109
|
+
let path = Self::value_to_string(path)?;
|
|
110
|
+
self.inner.to_ron_file(&path).map_err(to_ruby_err)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[magnus::wrap(class = "ENM::Enum", free_immediately, size)]
|
|
115
|
+
struct Enum {
|
|
116
|
+
inner: EnumType,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl Enum {
|
|
120
|
+
fn comment(&self) -> Option<String> {
|
|
121
|
+
self.inner.comment().map(|s| s.to_string())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn digest(&self) -> String {
|
|
125
|
+
self.inner.digest().to_string()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn each(ruby: &Ruby, rb_self: Value) -> Yield<std::vec::IntoIter<String>> {
|
|
129
|
+
if ruby.block_given() {
|
|
130
|
+
let wrapper = <&Enum>::try_convert(rb_self).expect("self is Enum");
|
|
131
|
+
|
|
132
|
+
Yield::Iter(wrapper.into_iter().cloned().collect::<Vec<_>>().into_iter())
|
|
133
|
+
} else {
|
|
134
|
+
Yield::Enumerator(rb_self.enumeratorize("each", ()))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn name(&self) -> String {
|
|
139
|
+
self.inner.name().to_string()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn schema(&self) -> String {
|
|
143
|
+
self.inner.schema().to_string()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn values(&self) -> Vec<String> {
|
|
147
|
+
self.inner.values().to_vec()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
impl<'a> IntoIterator for &'a Enum {
|
|
152
|
+
type Item = &'a String;
|
|
153
|
+
type IntoIter = std::slice::Iter<'a, String>;
|
|
154
|
+
|
|
155
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
156
|
+
(&self.inner).into_iter()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[magnus::init]
|
|
161
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
162
|
+
let module: RModule = ruby.define_module("ENM")?;
|
|
163
|
+
|
|
164
|
+
let error_class = module.define_error("Error", ruby.exception_standard_error())?;
|
|
165
|
+
|
|
166
|
+
module.define_error("DuplicatedTypeError", error_class)?;
|
|
167
|
+
module.define_error("DuplicatedValueError", error_class)?;
|
|
168
|
+
module.define_error("EnumConflictError", error_class)?;
|
|
169
|
+
module.define_error("EnumNotFoundError", error_class)?;
|
|
170
|
+
module.define_error("IOError", error_class)?;
|
|
171
|
+
module.define_error("ParseError", error_class)?;
|
|
172
|
+
module.define_error("SerializationError", error_class)?;
|
|
173
|
+
module.define_error("UnknownVersionError", error_class)?;
|
|
174
|
+
|
|
175
|
+
// Database class
|
|
176
|
+
let database_klass = module.define_class("Database", ruby.class_object())?;
|
|
177
|
+
|
|
178
|
+
database_klass.define_singleton_method("from_ron", function!(DatabaseWrapper::from_ron, 2))?;
|
|
179
|
+
database_klass.define_singleton_method("from_ron_file", function!(DatabaseWrapper::from_ron_file, 2))?;
|
|
180
|
+
database_klass.define_singleton_method("from_sql", function!(DatabaseWrapper::from_sql, 2))?;
|
|
181
|
+
database_klass.define_singleton_method("from_sql_file", function!(DatabaseWrapper::from_sql_file, 2))?;
|
|
182
|
+
database_klass.define_method("[]", method!(DatabaseWrapper::lazy_get, 1))?;
|
|
183
|
+
database_klass.define_method("name", method!(DatabaseWrapper::name, 0))?;
|
|
184
|
+
database_klass.define_method("empty?", method!(DatabaseWrapper::empty, 0))?;
|
|
185
|
+
database_klass.define_method("to_ron", method!(DatabaseWrapper::to_ron, 0))?;
|
|
186
|
+
database_klass.define_method("to_ron_file", method!(DatabaseWrapper::to_ron_file, 1))?;
|
|
187
|
+
database_klass.define_private_method("_get", method!(DatabaseWrapper::get, 2))?;
|
|
188
|
+
|
|
189
|
+
// Enum class with Enumerable
|
|
190
|
+
let enum_klass = module.define_class("Enum", ruby.class_object())?;
|
|
191
|
+
let enumerable: RModule = ruby.class_object().const_get("Enumerable")?;
|
|
192
|
+
|
|
193
|
+
enum_klass.include_module(enumerable)?;
|
|
194
|
+
|
|
195
|
+
enum_klass.define_method("comment", method!(Enum::comment, 0))?;
|
|
196
|
+
enum_klass.define_method("digest", method!(Enum::digest, 0))?;
|
|
197
|
+
enum_klass.define_method("each", method!(Enum::each, 0))?;
|
|
198
|
+
enum_klass.define_method("name", method!(Enum::name, 0))?;
|
|
199
|
+
enum_klass.define_method("schema", method!(Enum::schema, 0))?;
|
|
200
|
+
enum_klass.define_method("values", method!(Enum::values, 0))?;
|
|
201
|
+
|
|
202
|
+
if module.const_get::<_, Value>("RON_VERSION").is_err() {
|
|
203
|
+
module.const_set("RON_VERSION", RON_VERSION)?;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Ok(())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[cfg(test)]
|
|
210
|
+
mod tests {
|
|
211
|
+
use super::*;
|
|
212
|
+
use magnus::eval;
|
|
213
|
+
use rb_sys_test_helpers::ruby_test;
|
|
214
|
+
|
|
215
|
+
const VALID_SQL: &str = r#"
|
|
216
|
+
CREATE TYPE public.access_management AS ENUM ('global', 'contextual', 'forbidden');
|
|
217
|
+
COMMENT ON TYPE public.access_management IS 'Represents access management levels.';
|
|
218
|
+
CREATE TYPE public.analytics_context AS ENUM ('admin', 'frontend');
|
|
219
|
+
CREATE TYPE asset_kind AS ENUM ('unknown', 'image', 'video');
|
|
220
|
+
"#;
|
|
221
|
+
|
|
222
|
+
fn setup(ruby: &Ruby) {
|
|
223
|
+
init(ruby).expect("init failed");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn ruby_string_value(ruby: &Ruby, s: &str) -> Value {
|
|
227
|
+
ruby.str_new(s).as_value()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[ruby_test]
|
|
231
|
+
fn test_init_defines_enm_module() {
|
|
232
|
+
let ruby = Ruby::get().unwrap();
|
|
233
|
+
setup(&ruby);
|
|
234
|
+
|
|
235
|
+
let result: bool = eval("defined?(ENM) == 'constant'").unwrap();
|
|
236
|
+
|
|
237
|
+
assert!(result);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[ruby_test]
|
|
241
|
+
fn test_init_defines_error_classes() {
|
|
242
|
+
let ruby = Ruby::get().unwrap();
|
|
243
|
+
setup(&ruby);
|
|
244
|
+
|
|
245
|
+
for class_name in &[
|
|
246
|
+
"ENM::Error",
|
|
247
|
+
"ENM::DuplicatedTypeError",
|
|
248
|
+
"ENM::DuplicatedValueError",
|
|
249
|
+
"ENM::EnumConflictError",
|
|
250
|
+
"ENM::EnumNotFoundError",
|
|
251
|
+
"ENM::IOError",
|
|
252
|
+
"ENM::ParseError",
|
|
253
|
+
"ENM::SerializationError",
|
|
254
|
+
"ENM::UnknownVersionError",
|
|
255
|
+
] {
|
|
256
|
+
let result: bool = eval(&format!("defined?({class_name}) == 'constant'")).unwrap();
|
|
257
|
+
|
|
258
|
+
assert!(result, "{class_name} should be defined");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#[ruby_test]
|
|
263
|
+
fn test_init_sets_ron_version_constant() {
|
|
264
|
+
let ruby = Ruby::get().unwrap();
|
|
265
|
+
setup(&ruby);
|
|
266
|
+
|
|
267
|
+
let version: u32 = eval("ENM::RON_VERSION").unwrap();
|
|
268
|
+
assert_eq!(version, RON_VERSION);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[ruby_test]
|
|
272
|
+
fn test_database_from_sql() {
|
|
273
|
+
let ruby = Ruby::get().unwrap();
|
|
274
|
+
setup(&ruby);
|
|
275
|
+
|
|
276
|
+
let db = DatabaseWrapper::from_sql("test_db".to_string(), VALID_SQL.to_string()).unwrap();
|
|
277
|
+
assert_eq!(db.name(), "test_db");
|
|
278
|
+
assert!(!db.empty());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[ruby_test]
|
|
282
|
+
fn test_database_from_sql_empty() {
|
|
283
|
+
let ruby = Ruby::get().unwrap();
|
|
284
|
+
setup(&ruby);
|
|
285
|
+
|
|
286
|
+
let db = DatabaseWrapper::from_sql(
|
|
287
|
+
"empty_db".to_string(),
|
|
288
|
+
"CREATE TABLE t (id int);".to_string(),
|
|
289
|
+
)
|
|
290
|
+
.unwrap();
|
|
291
|
+
assert!(db.empty());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[ruby_test]
|
|
295
|
+
fn test_database_from_sql_invalid() {
|
|
296
|
+
let ruby = Ruby::get().unwrap();
|
|
297
|
+
|
|
298
|
+
setup(&ruby);
|
|
299
|
+
|
|
300
|
+
let result = DatabaseWrapper::from_sql("bad".to_string(), "not valid sql".to_string());
|
|
301
|
+
assert!(result.is_err());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[ruby_test]
|
|
305
|
+
fn test_database_ron_round_trip() {
|
|
306
|
+
let ruby = Ruby::get().unwrap();
|
|
307
|
+
|
|
308
|
+
setup(&ruby);
|
|
309
|
+
|
|
310
|
+
let db = DatabaseWrapper::from_sql("rt".to_string(), VALID_SQL.to_string()).unwrap();
|
|
311
|
+
let ron = db.to_ron().unwrap();
|
|
312
|
+
|
|
313
|
+
let db2 = DatabaseWrapper::from_ron(RON_VERSION, ron).unwrap();
|
|
314
|
+
|
|
315
|
+
assert_eq!(db2.name(), "rt");
|
|
316
|
+
assert!(!db2.empty());
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[ruby_test]
|
|
320
|
+
fn test_database_get_enum_by_name_and_schema() {
|
|
321
|
+
let ruby = Ruby::get().unwrap();
|
|
322
|
+
setup(&ruby);
|
|
323
|
+
|
|
324
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
325
|
+
let e = db
|
|
326
|
+
.get("access_management".to_string(), Some("public".to_string()))
|
|
327
|
+
.unwrap();
|
|
328
|
+
|
|
329
|
+
assert_eq!(e.name(), "access_management");
|
|
330
|
+
assert_eq!(e.schema(), "public");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[ruby_test]
|
|
334
|
+
fn test_database_get_enum_not_found() {
|
|
335
|
+
let ruby = Ruby::get().unwrap();
|
|
336
|
+
|
|
337
|
+
setup(&ruby);
|
|
338
|
+
|
|
339
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
340
|
+
let result = db.get("nonexistent".to_string(), None);
|
|
341
|
+
|
|
342
|
+
assert!(result.is_err());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[ruby_test]
|
|
346
|
+
fn test_lazy_get_with_symbol() {
|
|
347
|
+
let ruby = Ruby::get().unwrap();
|
|
348
|
+
|
|
349
|
+
setup(&ruby);
|
|
350
|
+
|
|
351
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
352
|
+
let key: Value = eval(":access_management").unwrap();
|
|
353
|
+
let e = db.lazy_get(key).unwrap();
|
|
354
|
+
|
|
355
|
+
assert_eq!(e.name(), "access_management");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#[ruby_test]
|
|
359
|
+
fn test_lazy_get_with_schema_dot_notation() {
|
|
360
|
+
let ruby = Ruby::get().unwrap();
|
|
361
|
+
|
|
362
|
+
setup(&ruby);
|
|
363
|
+
|
|
364
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
365
|
+
let key: Value = eval("'public.access_management'").unwrap();
|
|
366
|
+
let e = db.lazy_get(key).unwrap();
|
|
367
|
+
|
|
368
|
+
assert_eq!(e.name(), "access_management");
|
|
369
|
+
assert_eq!(e.schema(), "public");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[ruby_test]
|
|
373
|
+
fn test_lazy_get_with_string_no_schema() {
|
|
374
|
+
let ruby = Ruby::get().unwrap();
|
|
375
|
+
|
|
376
|
+
setup(&ruby);
|
|
377
|
+
|
|
378
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
379
|
+
let key: Value = eval("'access_management'").unwrap();
|
|
380
|
+
let e = db.lazy_get(key).unwrap();
|
|
381
|
+
|
|
382
|
+
assert_eq!(e.name(), "access_management");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[ruby_test]
|
|
386
|
+
fn test_enum_comment() {
|
|
387
|
+
let ruby = Ruby::get().unwrap();
|
|
388
|
+
|
|
389
|
+
setup(&ruby);
|
|
390
|
+
|
|
391
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
392
|
+
|
|
393
|
+
let with_comment = db
|
|
394
|
+
.get("access_management".to_string(), Some("public".to_string()))
|
|
395
|
+
.unwrap();
|
|
396
|
+
assert_eq!(
|
|
397
|
+
with_comment.comment(),
|
|
398
|
+
Some("Represents access management levels.".to_string())
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
let without_comment = db
|
|
402
|
+
.get("analytics_context".to_string(), Some("public".to_string()))
|
|
403
|
+
.unwrap();
|
|
404
|
+
|
|
405
|
+
assert_eq!(without_comment.comment(), None);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[ruby_test]
|
|
409
|
+
fn test_enum_each_with_block() {
|
|
410
|
+
let ruby = Ruby::get().unwrap();
|
|
411
|
+
|
|
412
|
+
setup(&ruby);
|
|
413
|
+
|
|
414
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
415
|
+
let e = db
|
|
416
|
+
.get("analytics_context".to_string(), Some("public".to_string()))
|
|
417
|
+
.unwrap();
|
|
418
|
+
|
|
419
|
+
let values: Vec<String> = e.values();
|
|
420
|
+
|
|
421
|
+
assert_eq!(values, vec!["admin", "frontend"]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#[ruby_test]
|
|
425
|
+
fn test_database_from_ron_rejects_bad_version() {
|
|
426
|
+
let ruby = Ruby::get().unwrap();
|
|
427
|
+
|
|
428
|
+
setup(&ruby);
|
|
429
|
+
|
|
430
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
431
|
+
let ron = db.to_ron().unwrap();
|
|
432
|
+
|
|
433
|
+
let result = DatabaseWrapper::from_ron(999, ron);
|
|
434
|
+
|
|
435
|
+
assert!(result.is_err());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[ruby_test]
|
|
439
|
+
fn test_database_from_sql_file() {
|
|
440
|
+
let ruby = Ruby::get().unwrap();
|
|
441
|
+
|
|
442
|
+
setup(&ruby);
|
|
443
|
+
|
|
444
|
+
let path = std::env::temp_dir().join("enm_test_from_sql_file.sql");
|
|
445
|
+
let path_str = path.to_str().unwrap();
|
|
446
|
+
|
|
447
|
+
std::fs::write(&path, VALID_SQL).unwrap();
|
|
448
|
+
|
|
449
|
+
let db = DatabaseWrapper::from_sql_file(
|
|
450
|
+
"test_db".to_string(),
|
|
451
|
+
ruby_string_value(&ruby, path_str),
|
|
452
|
+
)
|
|
453
|
+
.unwrap();
|
|
454
|
+
|
|
455
|
+
assert_eq!(db.name(), "test_db");
|
|
456
|
+
assert!(!db.empty());
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#[ruby_test]
|
|
460
|
+
fn test_database_from_sql_file_not_found() {
|
|
461
|
+
let ruby = Ruby::get().unwrap();
|
|
462
|
+
|
|
463
|
+
setup(&ruby);
|
|
464
|
+
|
|
465
|
+
let result = DatabaseWrapper::from_sql_file(
|
|
466
|
+
"test".to_string(),
|
|
467
|
+
ruby_string_value(&ruby, "/nonexistent/path/enm_test.sql"),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
assert!(result.is_err());
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
#[ruby_test]
|
|
474
|
+
fn test_database_from_ron_file() {
|
|
475
|
+
let ruby = Ruby::get().unwrap();
|
|
476
|
+
|
|
477
|
+
setup(&ruby);
|
|
478
|
+
|
|
479
|
+
let db = DatabaseWrapper::from_sql("rt".to_string(), VALID_SQL.to_string()).unwrap();
|
|
480
|
+
let ron = db.to_ron().unwrap();
|
|
481
|
+
let path = std::env::temp_dir().join("enm_test_from_ron_file.ron");
|
|
482
|
+
let path_str = path.to_str().unwrap();
|
|
483
|
+
|
|
484
|
+
std::fs::write(&path, &ron).unwrap();
|
|
485
|
+
|
|
486
|
+
let db2 = DatabaseWrapper::from_ron_file(RON_VERSION, ruby_string_value(&ruby, path_str))
|
|
487
|
+
.unwrap();
|
|
488
|
+
|
|
489
|
+
assert_eq!(db2.name(), "rt");
|
|
490
|
+
assert!(!db2.empty());
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#[ruby_test]
|
|
494
|
+
fn test_database_from_ron_file_not_found() {
|
|
495
|
+
let ruby = Ruby::get().unwrap();
|
|
496
|
+
|
|
497
|
+
setup(&ruby);
|
|
498
|
+
|
|
499
|
+
let result = DatabaseWrapper::from_ron_file(
|
|
500
|
+
RON_VERSION,
|
|
501
|
+
ruby_string_value(&ruby, "/nonexistent/path/enm_test.ron"),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
assert!(result.is_err());
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#[ruby_test]
|
|
508
|
+
fn test_database_to_ron_file() {
|
|
509
|
+
let ruby = Ruby::get().unwrap();
|
|
510
|
+
|
|
511
|
+
setup(&ruby);
|
|
512
|
+
|
|
513
|
+
let db = DatabaseWrapper::from_sql("test".to_string(), VALID_SQL.to_string()).unwrap();
|
|
514
|
+
let path = std::env::temp_dir().join("enm_test_to_ron_file.ron");
|
|
515
|
+
let path_str = path.to_str().unwrap();
|
|
516
|
+
|
|
517
|
+
db.to_ron_file(ruby_string_value(&ruby, path_str)).unwrap();
|
|
518
|
+
|
|
519
|
+
let db2 = DatabaseWrapper::from_ron_file(RON_VERSION, ruby_string_value(&ruby, path_str))
|
|
520
|
+
.unwrap();
|
|
521
|
+
|
|
522
|
+
assert_eq!(db2.name(), "test");
|
|
523
|
+
assert!(!db2.empty());
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#[ruby_test]
|
|
527
|
+
fn test_database_to_ron_file_round_trip() {
|
|
528
|
+
let ruby = Ruby::get().unwrap();
|
|
529
|
+
|
|
530
|
+
setup(&ruby);
|
|
531
|
+
|
|
532
|
+
let db = DatabaseWrapper::from_sql("rt".to_string(), VALID_SQL.to_string()).unwrap();
|
|
533
|
+
let expected_ron = db.to_ron().unwrap();
|
|
534
|
+
let path = std::env::temp_dir().join("enm_test_to_ron_file_rt.ron");
|
|
535
|
+
let path_str = path.to_str().unwrap();
|
|
536
|
+
|
|
537
|
+
db.to_ron_file(ruby_string_value(&ruby, path_str)).unwrap();
|
|
538
|
+
let db2 = DatabaseWrapper::from_ron_file(RON_VERSION, ruby_string_value(&ruby, path_str))
|
|
539
|
+
.unwrap();
|
|
540
|
+
|
|
541
|
+
assert_eq!(db2.to_ron().unwrap(), expected_ron);
|
|
542
|
+
}
|
|
543
|
+
}
|
data/lib/enm/database.rb
ADDED
data/lib/enm/enum.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ENM
|
|
4
|
+
# An enum type from the database.
|
|
5
|
+
class Enum
|
|
6
|
+
extend Dry::Core::Cache
|
|
7
|
+
|
|
8
|
+
# @!parse [ruby]
|
|
9
|
+
# include Enumerable
|
|
10
|
+
|
|
11
|
+
# @!attribute [r] comment
|
|
12
|
+
# @return [String, nil] comment associated with the enum type
|
|
13
|
+
|
|
14
|
+
# @!attribute [r] digest
|
|
15
|
+
# @api private
|
|
16
|
+
# @return [String] a digest representing the enum values, for caching purposes
|
|
17
|
+
|
|
18
|
+
# @!attribute [r] name
|
|
19
|
+
# @return [String] the name of the enum type
|
|
20
|
+
|
|
21
|
+
# @!attribute [r] schema
|
|
22
|
+
# @return [String] the schema of the enum type
|
|
23
|
+
|
|
24
|
+
# @see ENM::Enum::DryTypeBuilder
|
|
25
|
+
# @param [String, Symbol, #to_s, #to_sym, nil] default whether to use a
|
|
26
|
+
# certain value as dry-type's default, since it must be defined _before_
|
|
27
|
+
# the enum call.
|
|
28
|
+
# @param [:string, :symbol] mode the underlying type of the enum values.
|
|
29
|
+
# @return [Dry::Types::Type]
|
|
30
|
+
def dry(default: nil, mode: :string)
|
|
31
|
+
fetch_or_store([digest, default, mode]) do
|
|
32
|
+
DryTypeBuilder.new(self, mode:, default:).()
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Builds a `Dry::Types::Type` for an {ENM::Enum}.
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
# @see ENM::Enum#dry
|
|
40
|
+
class DryTypeBuilder
|
|
41
|
+
include Dry::Initializer[undefined: false].define -> do
|
|
42
|
+
param :enum, ENM::Types::Enum
|
|
43
|
+
|
|
44
|
+
option :mode, ENM::Types::DryMode
|
|
45
|
+
|
|
46
|
+
option :default, ENM::Types::DefaultValue.optional, as: :default_value, optional: true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [String, Symbol, nil]
|
|
50
|
+
attr_reader :default
|
|
51
|
+
|
|
52
|
+
def initialize(...)
|
|
53
|
+
super
|
|
54
|
+
|
|
55
|
+
@default = derive_real_default
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Dry::Types::Type]
|
|
59
|
+
def call
|
|
60
|
+
base = build_base
|
|
61
|
+
|
|
62
|
+
type = default? ? base.default(default.freeze) : base
|
|
63
|
+
|
|
64
|
+
enum_values = symbol? ? enum.map(&:to_sym) : enum.to_a
|
|
65
|
+
|
|
66
|
+
type.enum(*enum_values)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default? = !default_value.nil?
|
|
70
|
+
|
|
71
|
+
def symbol? = mode == :symbol
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# @return [Dry::Types::Type]
|
|
76
|
+
def build_base = symbol? ? ENM::Types::Coercible::Symbol : ENM::Types::Coercible::String
|
|
77
|
+
|
|
78
|
+
# @return [String, Symbol, nil]
|
|
79
|
+
def derive_real_default
|
|
80
|
+
return unless default?
|
|
81
|
+
|
|
82
|
+
unless enum.include?(default_value)
|
|
83
|
+
raise ENM::InvalidDefaultError, "Default #{default_value.inspect} not present in #{enum.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
symbol? ? default_value.to_sym : default_value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|