Skip to content

Commit

Permalink
feat: add options "--readonly-*"
Browse files Browse the repository at this point in the history
to set tables to only generate StructType::Read

re Wulf#56
  • Loading branch information
hasezoey committed Oct 29, 2023
1 parent 1e0b4d6 commit ab051c1
Show file tree
Hide file tree
Showing 16 changed files with 369 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- add option `create-str` to set `Create*` structs string type
- add option `update-str` to set `Update*` structs string type
- add option `--single-model-file` to only generate a single file instead of a directory with `mod.rs` and `generated.rs`
- add option `--readonly-prefix` and `--readonly-suffix` to treat a matching name as a readonly struct
- derive generation has been refactored and now only necessary derives are added to a given struct

## 0.0.16
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ cargo install dsync
* `-i`: input argument: path to schema file
* `-o`: output argument: path to directory where generated code should be written
* `-c`: connection type (for example: `diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>`)
* `-g`: (optional) list of columns that are automatically generated by create/update triggers (for example, `created_at`, `updated_at`)
* `-g`: (optional, repeatable) list of columns that are automatically generated by create/update triggers (for example, `created_at`, `updated_at`)
* `--tsync`: (optional) adds `#[tsync]` attribute to generated structs (see <https://github.com/Wulf/tsync>)
* `--model-path`: (optional) set a custom model import path, default `crate::models::`
* `--schema-path`: (optional) set a custom schema import path, default `crate::schema::`
Expand All @@ -118,12 +118,19 @@ cargo install dsync
* `--create-str`: (optional) Set which string type to use for `Create*` structs (possible are `string`, `str`, `cow`)
* `--update-str`: (optional) Set which string type to use for `Update*` structs (possible are `string`, `str`, `cow`)
* `--single-model-file`: (optional) Generate only a single model file, instead of a directory with `mod.rs` and `generated.rs`
* `--readonly-prefix`: (optional, repeatable) A prefix to treat a table matching this as readonly *2
* `--readonly-suffix`: (optional, repeatable) A suffix to treat a table matching this as readonly *2
* note: the CLI has fail-safes to prevent accidental file overwriting
```sh
dsync -i src/schema.rs -o src/models
```
Notes:
- *2: "readonly" tables dont have `Update*` & `Create*` structs, only `*`(no suffix / prefix) structs.
For example this is useful for Sqlite views, which are read-only (cannot be written to, but can be read)
## Docs
See `dsync --help` for more information.
Expand Down
10 changes: 10 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ pub struct MainOptions {
/// Generate the "ConnectionType" type only once in a "common.rs" file
#[arg(long = "once-connection-type")]
pub once_connection_type: bool,

/// A Prefix to treat a table matching this as readonly (only generate the Read struct)
#[arg(long = "readonly-prefix")]
pub readonly_prefixes: Vec<String>,

/// A Suffix to treat a table matching this as readonly (only generate the Read struct)
#[arg(long = "readonly-suffix")]
pub readonly_suffixes: Vec<String>,
}

#[derive(Debug, ValueEnum, Clone, PartialEq, Default)]
Expand Down Expand Up @@ -203,6 +211,8 @@ fn actual_main() -> dsync::Result<()> {
model_path: args.model_path,
once_common_structs: args.once_common_structs,
once_connection_type: args.once_connection_type,
readonly_prefixes: args.readonly_prefixes,
readonly_suffixes: args.readonly_suffixes,
},
)?;

Expand Down
37 changes: 27 additions & 10 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,18 @@ impl<'a> Struct<'a> {
let ty = self.ty;
let table = &self.table;

if self.opts.get_readonly() {
match ty {
StructType::Read => (),
StructType::Update | StructType::Create => {
self.has_fields = Some(false);
self.rendered_code = None;

return;
}
}
}

let primary_keys: Vec<String> = table.primary_key_column_names();

let belongs_to = table
Expand Down Expand Up @@ -402,6 +414,7 @@ fn build_table_fns(
let schema_path = &config.schema_path;
let create_struct_identifier = &create_struct.identifier;
let update_struct_identifier = &update_struct.identifier;
let is_readonly = table_options.get_readonly();

let mut buffer = String::new();

Expand All @@ -415,8 +428,9 @@ impl {struct_name} {{
"##
));

if create_struct.has_fields() {
buffer.push_str(&format!(
if !is_readonly {
if create_struct.has_fields() {
buffer.push_str(&format!(
r##"
pub{async_keyword} fn create(db: &mut ConnectionType, item: &{create_struct_identifier}) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;
Expand All @@ -425,16 +439,17 @@ impl {struct_name} {{
}}
"##
));
} else {
buffer.push_str(&format!(
r##"
} else {
buffer.push_str(&format!(
r##"
pub{async_keyword} fn create(db: &mut ConnectionType) -> QueryResult<Self> {{
use {schema_path}{table_name}::dsl::*;
insert_into({table_name}).default_values().get_result::<Self>(db){await_keyword}
}}
"##
));
));
}
}

buffer.push_str(&format!(
Expand Down Expand Up @@ -471,7 +486,7 @@ impl {struct_name} {{
// then don't require item_id_params (otherwise it'll be duplicated)

// if has_update_struct {
if update_struct.has_fields() {
if update_struct.has_fields() && !is_readonly {
// It's possible we have a form struct with all primary keys (for example, for a join table).
// In this scenario, we also have to check whether there are any updatable columns for which
// we should generate an update() method.
Expand All @@ -485,15 +500,17 @@ impl {struct_name} {{
"##));
}

buffer.push_str(&format!(
r##"
if !is_readonly {
buffer.push_str(&format!(
r##"
pub{async_keyword} fn delete(db: &mut ConnectionType, {item_id_params}) -> QueryResult<usize> {{
use {schema_path}{table_name}::dsl::*;
diesel::delete({table_name}.{item_id_filters}).execute(db){await_keyword}
}}
"##
));
));
}

buffer.push_str(
r##"
Expand Down
29 changes: 27 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pub struct TableOptions<'a> {

/// Only Generate a single model file instead of a directory with "mod.rs" and "generated.rs"
single_model_file: bool,

/// Indiciates this table is meant to be read-only (dont generate Update & Create structs)
read_only: bool,
}

impl<'a> TableOptions<'a> {
Expand Down Expand Up @@ -123,6 +126,10 @@ impl<'a> TableOptions<'a> {
self.autogenerated_columns.as_deref().unwrap_or_default()
}

pub fn get_readonly(&self) -> bool {
self.read_only
}

pub fn ignore(self) -> Self {
Self {
ignore: Some(true),
Expand Down Expand Up @@ -185,6 +192,10 @@ impl<'a> TableOptions<'a> {
}
}

pub fn set_read_only(&mut self, value: bool) {
self.read_only = value;
}

/// Fills any `None` properties with values from another TableConfig
pub fn apply_defaults(&self, other: &TableOptions<'a>) -> Self {
Self {
Expand All @@ -203,6 +214,7 @@ impl<'a> TableOptions<'a> {
create_str_type: other.create_str_type,
update_str_type: other.update_str_type,
single_model_file: self.single_model_file || other.single_model_file,
read_only: self.read_only || other.read_only,
}
}
}
Expand All @@ -221,6 +233,7 @@ impl<'a> Default for TableOptions<'a> {
create_str_type: Default::default(),
update_str_type: Default::default(),
single_model_file: false,
read_only: false,
}
}
}
Expand Down Expand Up @@ -248,16 +261,28 @@ pub struct GenerationConfig<'a> {
pub once_common_structs: bool,
/// Generate the "ConnectionType" type only once in a "common.rs" file
pub once_connection_type: bool,
/// Prefixes to treat tables as readonly
pub readonly_prefixes: Vec<String>,
/// Suffixes to treat tables as readonly
pub readonly_suffixes: Vec<String>,
}

impl GenerationConfig<'_> {
pub fn table(&self, name: &str) -> TableOptions<'_> {
let t = self
let table = self
.table_options
.get(name)
.unwrap_or(&self.default_table_options);

t.apply_defaults(&self.default_table_options)
let mut table = table.apply_defaults(&self.default_table_options);

if self.readonly_prefixes.iter().any(|v| name.starts_with(v))
|| self.readonly_suffixes.iter().any(|v| name.ends_with(v))
{
table.set_read_only(true);
}

table
}
}

Expand Down
4 changes: 4 additions & 0 deletions test/readonly/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod normal;
pub mod prefixTable;
pub mod tableSuffix;
pub mod prefixTableSuffix;
85 changes: 85 additions & 0 deletions test/readonly/models/normal/generated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* @generated and managed by dsync */

use crate::diesel::*;
use crate::schema::*;
use diesel::QueryResult;
use serde::{Deserialize, Serialize};


type ConnectionType = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;

#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable)]
#[diesel(table_name=normal, primary_key(id))]
pub struct Normal {
pub id: crate::schema::sql_types::Int,
pub testprop: crate::schema::sql_types::Int,
}

#[derive(Debug, Clone, Serialize, Deserialize, Insertable)]
#[diesel(table_name=normal)]
pub struct CreateNormal {
pub testprop: crate::schema::sql_types::Int,
}

#[derive(Debug, Clone, Serialize, Deserialize, AsChangeset, Default)]
#[diesel(table_name=normal)]
pub struct UpdateNormal {
pub testprop: Option<crate::schema::sql_types::Int>,
}


#[derive(Debug, Serialize)]
pub struct PaginationResult<T> {
pub items: Vec<T>,
pub total_items: i64,
/// 0-based index
pub page: i64,
pub page_size: i64,
pub num_pages: i64,
}

impl Normal {

pub fn create(db: &mut ConnectionType, item: &CreateNormal) -> QueryResult<Self> {
use crate::schema::normal::dsl::*;

insert_into(normal).values(item).get_result::<Self>(db)
}

pub fn read(db: &mut ConnectionType, param_id: crate::schema::sql_types::Int) -> QueryResult<Self> {
use crate::schema::normal::dsl::*;

normal.filter(id.eq(param_id)).first::<Self>(db)
}

/// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64) -> QueryResult<PaginationResult<Self>> {
use crate::schema::normal::dsl::*;

let page_size = if page_size < 1 { 1 } else { page_size };
let total_items = normal.count().get_result(db)?;
let items = normal.limit(page_size).offset(page * page_size).load::<Self>(db)?;

Ok(PaginationResult {
items,
total_items,
page,
page_size,
/* ceiling division of integers */
num_pages: total_items / page_size + i64::from(total_items % page_size != 0)
})
}

pub fn update(db: &mut ConnectionType, param_id: crate::schema::sql_types::Int, item: &UpdateNormal) -> QueryResult<Self> {
use crate::schema::normal::dsl::*;

diesel::update(normal.filter(id.eq(param_id))).set(item).get_result(db)
}

pub fn delete(db: &mut ConnectionType, param_id: crate::schema::sql_types::Int) -> QueryResult<usize> {
use crate::schema::normal::dsl::*;

diesel::delete(normal.filter(id.eq(param_id))).execute(db)
}

}
2 changes: 2 additions & 0 deletions test/readonly/models/normal/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod generated;
pub use generated::*;
55 changes: 55 additions & 0 deletions test/readonly/models/prefixTable/generated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* @generated and managed by dsync */

use crate::diesel::*;
use crate::schema::*;
use diesel::QueryResult;
use serde::{Deserialize, Serialize};


type ConnectionType = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;

#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable)]
#[diesel(table_name=prefixTable, primary_key(id))]
pub struct PrefixTable {
pub id: crate::schema::sql_types::Int,
pub testprop: crate::schema::sql_types::Int,
}


#[derive(Debug, Serialize)]
pub struct PaginationResult<T> {
pub items: Vec<T>,
pub total_items: i64,
/// 0-based index
pub page: i64,
pub page_size: i64,
pub num_pages: i64,
}

impl PrefixTable {

pub fn read(db: &mut ConnectionType, param_id: crate::schema::sql_types::Int) -> QueryResult<Self> {
use crate::schema::prefixTable::dsl::*;

prefixTable.filter(id.eq(param_id)).first::<Self>(db)
}

/// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64) -> QueryResult<PaginationResult<Self>> {
use crate::schema::prefixTable::dsl::*;

let page_size = if page_size < 1 { 1 } else { page_size };
let total_items = prefixTable.count().get_result(db)?;
let items = prefixTable.limit(page_size).offset(page * page_size).load::<Self>(db)?;

Ok(PaginationResult {
items,
total_items,
page,
page_size,
/* ceiling division of integers */
num_pages: total_items / page_size + i64::from(total_items % page_size != 0)
})
}

}
2 changes: 2 additions & 0 deletions test/readonly/models/prefixTable/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod generated;
pub use generated::*;
Loading

0 comments on commit ab051c1

Please sign in to comment.