Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify stub module public API #79

Merged
merged 5 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,6 @@ impl App {
PublicHandle::from_str(&content)
}

fn build_stub_config(&self, args: &ArgMatches) -> Result<StubConfig> {
let lang_arg = args
.get_one::<String>("PROGRAMMING_LANGUAGE")
.context("Should have a programming language")?;

StubConfig::find_stub_config(lang_arg.as_str(), &self.stub_templates_dir)
}

fn clashes(&self) -> Result<std::fs::ReadDir> {
std::fs::read_dir(&self.clash_dir).with_context(|| "No clashes stored")
}
Expand Down Expand Up @@ -460,7 +452,9 @@ impl App {
}

fn generate_stub(&self, args: &ArgMatches) -> Result<()> {
let config = self.build_stub_config(args)?;
let lang_arg = args
.get_one::<String>("PROGRAMMING_LANGUAGE")
.context("Should have a programming language")?;

let stub_generator = match args.get_one::<PathBuf>("from-file") {
Some(fname) if fname.to_str() == Some("-") => {
Expand All @@ -479,7 +473,18 @@ impl App {
}
};

let stub_string = stub::generate(config, &stub_generator)?;
// Language config files are stored in: (ordered by precedence)
// 1. The user config dir, where {CONF} is the OS dependent config folder:
// `{CONF}/stub_templates/LANG/stub_config.toml`
// 2. This repo, embedded into the binary:
// `config/stub_templates/LANG/stub_config.toml`
let lang_template_dir = self.stub_templates_dir.join(lang_arg);
let stub_string = if lang_template_dir.is_dir() {
let stub_config = StubConfig::read_from_dir(lang_template_dir)?;
stub::generate_from_config(stub_config, &stub_generator)?
} else {
stub::generate(lang_arg, &stub_generator)?
};
println!("{stub_string}");
Ok(())
}
Expand Down
69 changes: 39 additions & 30 deletions src/stub.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
pub mod language;
mod language;
mod parser;
pub mod preprocessor;
mod preprocessor;
mod renderer;
pub mod stub_config;
mod stub_config;

use anyhow::Result;
use indoc::indoc;
pub use language::Language;
use language::Language;
use preprocessor::Renderable;
use serde::Serialize;
pub use stub_config::StubConfig;

pub fn generate(config: StubConfig, generator: &str) -> Result<String> {
pub fn generate_from_config(config: StubConfig, generator: &str) -> Result<String> {
let mut stub = parser::parse_generator_stub(generator)?;

if let Some(processor) = config.language.preprocessor {
processor(&mut stub)
}

// eprint!("=======\n{:?}\n======\n", generator);
// eprint!("=======\n{:?}\n======\n", stub);

let output_str = renderer::render_stub(config.clone(), stub)?;
let renderer = renderer::Renderer::new(config, stub)?;
let output_str = renderer.render();

Ok(output_str.as_str().trim().to_string())
}

/// Generate a stub string from a (supported) language and a generator.
///
/// # Examples
///
/// ```
/// use clashlib::stub::generate;
///
/// let generator = "read anInt:int\nwrite solution";
/// let stub_str = generate("python", generator).unwrap();
/// assert_eq!(stub_str, "an_int = int(input())\nprint(\"solution\")");
/// ```
pub fn generate(language_name: &str, generator: &str) -> Result<String> {
let config = StubConfig::read_from_embedded(language_name)?;
generate_from_config(config, generator)
}

#[derive(Clone, Default)]
pub struct Stub {
pub commands: Vec<Cmd>,
pub statement: Vec<String>,
struct Stub {
commands: Vec<Cmd>,
statement: Vec<String>,
}

// More visual than derive(Debug)
Expand All @@ -44,7 +58,7 @@ impl std::fmt::Debug for Stub {
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
pub enum VarType {
enum VarType {
Int,
Float,
Long,
Expand Down Expand Up @@ -74,11 +88,11 @@ impl<'a> VarType {
}

#[derive(Debug, Clone, Serialize)]
pub struct VariableCommand {
pub ident: String,
pub var_type: VarType,
pub max_length: Option<String>,
pub input_comment: String,
struct VariableCommand {
ident: String,
var_type: VarType,
max_length: Option<String>,
input_comment: String,
}

impl VariableCommand {
Expand All @@ -93,7 +107,7 @@ impl VariableCommand {
}

#[derive(Serialize, Clone, Debug)]
pub struct JoinTerm {
struct JoinTerm {
pub ident: String,
pub var_type: Option<VarType>,
}
Expand All @@ -105,7 +119,7 @@ impl JoinTerm {
}

#[derive(Debug, Clone)]
pub enum Cmd {
enum Cmd {
Read(Vec<VariableCommand>),
Loop {
count_var: String,
Expand Down Expand Up @@ -222,18 +236,16 @@ mod tests {

#[test]
fn test_simple_code_generation() {
let cfg = StubConfig::read_from_embedded("ruby").unwrap();
let generator = "read m:int n:int\nwrite result";
let received = generate(cfg, generator).unwrap();
let received = generate("ruby", generator).unwrap();
let expected = "m, n = gets.split.map(&:to_i)\nputs \"result\"";

assert_eq!(received, expected);
}

#[test]
fn test_reference_stub_ruby() {
let cfg = StubConfig::read_from_embedded("ruby").unwrap();
let received = generate(cfg, COMPLEX_REFERENCE_STUB).unwrap();
let received = generate("ruby", COMPLEX_REFERENCE_STUB).unwrap();
let expected = indoc! { r##"
# Live long
# and prosper
Expand Down Expand Up @@ -298,19 +310,16 @@ mod tests {
// Just test that it compiles
#[test]
fn test_reference_stub_rust() {
let cfg = StubConfig::read_from_embedded("rust").unwrap();
generate(cfg, COMPLEX_REFERENCE_STUB).unwrap();
generate("rust", COMPLEX_REFERENCE_STUB).unwrap();
}

#[test]
fn test_reference_stub_c() {
let cfg = StubConfig::read_from_embedded("c").unwrap();
generate(cfg, COMPLEX_REFERENCE_STUB).unwrap();
generate("c", COMPLEX_REFERENCE_STUB).unwrap();
}

#[test]
fn test_reference_stub_cpp() {
let cfg = StubConfig::read_from_embedded("cpp").unwrap();
generate(cfg, COMPLEX_REFERENCE_STUB).unwrap();
generate("cpp", COMPLEX_REFERENCE_STUB).unwrap();
}
}
17 changes: 8 additions & 9 deletions src/stub/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@ use super::preprocessor::{self, Preprocessor};

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct TypeTokens {
pub int: Option<String>,
pub float: Option<String>,
pub long: Option<String>,
pub bool: Option<String>,
pub word: Option<String>,
pub string: Option<String>,
pub(super) struct TypeTokens {
int: Option<String>,
float: Option<String>,
long: Option<String>,
bool: Option<String>,
word: Option<String>,
string: Option<String>,
}

#[derive(Deserialize, Clone, Debug)]
pub struct Language {
pub name: String,
pub(super) struct Language {
pub variable_name_options: VariableNameOptions,
pub source_file_ext: String,
pub type_tokens: TypeTokens,
Expand Down
14 changes: 7 additions & 7 deletions src/stub/language/variable_name_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::stub::VariableCommand;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
pub enum Casing {
enum Casing {
SnakeCase,
KebabCase,
CamelCase,
Expand All @@ -16,17 +16,17 @@ pub enum Casing {
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct VariableNameOptions {
pub casing: Casing,
pub allow_uppercase_vars: Option<bool>,
pub keywords: Vec<String>,
casing: Casing,
allow_uppercase_vars: Option<bool>,
keywords: Vec<String>,
}

fn is_uppercase_string(string: &str) -> bool {
string.chars().all(|c| c.is_uppercase())
}

impl VariableNameOptions {
pub fn transform_variable_name(&self, variable_name: &str) -> String {
pub(in crate::stub) fn transform_variable_name(&self, variable_name: &str) -> String {
// CG has special treatment for variables with all uppercase identifiers.
// In most languages they remain uppercase regardless of variable format.
// In others (such as ruby where constants are uppercase) they get downcased.
Expand All @@ -39,7 +39,7 @@ impl VariableNameOptions {
self.escape_keywords(converted_variable_name)
}

pub fn transform_variable_command(&self, var: &VariableCommand) -> VariableCommand {
pub(in crate::stub) fn transform_variable_command(&self, var: &VariableCommand) -> VariableCommand {
VariableCommand {
ident: self.transform_variable_name(&var.ident),
var_type: var.var_type,
Expand All @@ -48,7 +48,7 @@ impl VariableNameOptions {
}
}

pub fn escape_keywords(&self, variable_name: String) -> String {
fn escape_keywords(&self, variable_name: String) -> String {
if self.keywords.contains(&variable_name) {
format!("_{variable_name}")
} else {
Expand Down
13 changes: 4 additions & 9 deletions src/stub/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,22 @@ const ALPHABET: [char; 18] = [
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
];

pub fn render_stub(config: StubConfig, stub: Stub) -> Result<String> {
let renderer = Renderer::new(config, stub)?;
Ok(renderer.render())
}

pub struct Renderer {
tera: Tera,
lang: Language,
stub: Stub,
}

impl Renderer {
fn new(config: StubConfig, stub: Stub) -> Result<Renderer> {
pub(super) fn new(config: StubConfig, stub: Stub) -> Result<Renderer> {
Ok(Self {
lang: config.language,
tera: config.tera,
stub,
})
}

pub fn tera_render(&self, template_name: &str, context: &mut Context) -> String {
pub(super) fn tera_render(&self, template_name: &str, context: &mut Context) -> String {
// Since these are (generally) shared across languages, it makes sense to
// store it in the "global" context instead of accepting it as parameters.
let format_symbols = json!({
Expand All @@ -50,7 +45,7 @@ impl Renderer {
.unwrap()
}

fn render(&self) -> String {
pub(super) fn render(&self) -> String {
let mut context = Context::new();

let code: String = self.stub.commands.iter().map(|cmd| self.render_command(cmd, 0)).collect();
Expand All @@ -62,7 +57,7 @@ impl Renderer {
self.tera_render("main", &mut context)
}

pub fn render_command(&self, cmd: &Cmd, nesting_depth: usize) -> String {
pub(super) fn render_command(&self, cmd: &Cmd, nesting_depth: usize) -> String {
match cmd {
Cmd::Read(vars) => self.render_read(vars, nesting_depth),
Cmd::Write {
Expand Down
40 changes: 9 additions & 31 deletions src/stub/stub_config.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,42 @@
use std::fs;
use std::path::Path;

use anyhow::{Context, Result};
use include_dir::include_dir;
use tera::Tera;

use super::Language;

const HARDCODED_TEMPLATE_DIR: include_dir::Dir<'static> =
const HARDCODED_EMBEDDED_TEMPLATE_DIR: include_dir::Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/config/stub_templates");

#[derive(Clone)]
pub struct StubConfig {
pub language: Language,
pub tera: Tera,
pub(super) language: Language,
pub(super) tera: Tera,
}

impl StubConfig {
/// This function is responsible for searching locations where language
/// config files can be stored.
///
/// Language config files are stored in: (ordered by precedence)
/// 1. The user config dir: `stub_templates/#{lang_arg}/stub_config.toml`
/// 2. This repo, embedded into the binary:
/// `config/stub_templates/#{lang_arg}/stub_config.toml`
///
/// where the user config dir is in `~/.config/coctus` (Linux, see the
/// [directories documentation](https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_dir)
/// for others).
pub fn find_stub_config(lang_name: &str, config_path: &Path) -> Result<Self> {
let user_config_lang_dir = config_path.join(lang_name);

if user_config_lang_dir.is_file() {
Self::read_from_dir(user_config_lang_dir)
} else {
Self::read_from_embedded(&lang_name.to_lowercase())
}
}

pub fn read_from_dir(dir: std::path::PathBuf) -> Result<Self> {
let fname = dir.join("stub_config.toml");
let toml_str = fs::read_to_string(fname)?;
let toml_file = dir.join("stub_config.toml");
let toml_str = fs::read_to_string(toml_file)?;
let language: Language = toml::from_str(&toml_str)?;
let jinja_glob = dir.join("*.jinja");
let tera = Tera::new(jinja_glob.to_str().expect("language directory path should be valid utf8"))
.context("Failed to create Tera instance")?;
Ok(Self { language, tera })
}

pub fn read_from_embedded(lang_name: &str) -> Result<Self> {
pub(super) fn read_from_embedded(lang_name: &str) -> Result<Self> {
// If you just created a new template for a language and you get:
// Error: No stub generator found for 'language'
// you may need to recompile the binaries to update: `cargo build`
let embedded_config_dir = HARDCODED_TEMPLATE_DIR
let embedded_config_dir = HARDCODED_EMBEDDED_TEMPLATE_DIR
.get_dir(lang_name)
.context(format!("No stub generator found for '{lang_name}'"))?;
let config_file = embedded_config_dir
let toml_file = embedded_config_dir
.get_file(format!("{lang_name}/stub_config.toml"))
.expect("Embedded stub generators should have stub_config.toml");
let toml_str = config_file
let toml_str = toml_file
.contents_utf8()
.expect("Embedded stub_config.toml contents should be valid utf8");
let language: Language = toml::from_str(toml_str)?;
Expand Down
Loading