diff --git a/.github/workflows/pkgdown-gh-pages.yaml b/.github/workflows/pkgdown-gh-pages.yaml index 7df698d..d7193da 100644 --- a/.github/workflows/pkgdown-gh-pages.yaml +++ b/.github/workflows/pkgdown-gh-pages.yaml @@ -1,42 +1,42 @@ -# Alternative pkgdown workflow using gh-pages branch deployment -# This workflow builds the pkgdown site and deploys to gh-pages branch -# rather than using the newer GitHub Actions deployment method - -on: - push: - branches: [main] - workflow_dispatch: - -name: pkgdown-gh-pages - -jobs: - pkgdown: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::pkgdown, local::. - needs: website - - - name: Build site - run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) - shell: Rscript {0} - - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 - with: - folder: docs - branch: gh-pages +# Alternative pkgdown workflow using gh-pages branch deployment +# This workflow builds the pkgdown site and deploys to gh-pages branch +# rather than using the newer GitHub Actions deployment method + +on: + push: + branches: [main] + workflow_dispatch: + +name: pkgdown-gh-pages + +jobs: + pkgdown: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Build site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: docs + branch: gh-pages clean: true \ No newline at end of file diff --git a/inst/examples/create-putior-hex.R b/inst/examples/create-putior-hex.R index 53320e0..a5683fc 100644 --- a/inst/examples/create-putior-hex.R +++ b/inst/examples/create-putior-hex.R @@ -1,223 +1,223 @@ -# Create putior hex sticker - Recommended ggplot2 approach -# This script recreates the putior hex sticker using pure ggplot2 - -library(ggplot2) -library(dplyr) - -# Function to create hexagon coordinates -create_hexagon <- function(center_x = 0.5, center_y = 0.5, size = 0.45) { - angles <- seq(30, 330, by = 60) * pi / 180 - data.frame( - x = center_x + size * cos(angles), - y = center_y + size * sin(angles) - ) -} - -# Create network data (mimicking the original design) -create_network_data <- function() { - set.seed(42) # For reproducibility - - # Node positions (right side of hex) - nodes <- data.frame( - x = c(0.65, 0.75, 0.85, 0.7, 0.8, 0.6, 0.9), - y = c(0.8, 0.7, 0.6, 0.5, 0.4, 0.6, 0.7), - size = c(4, 3.5, 3, 3.5, 3, 2.5, 2.5), - id = 1:7 - ) - - # Connections between nodes - edges <- data.frame( - from = c(1, 1, 2, 3, 4, 6), - to = c(2, 4, 3, 5, 5, 7) - ) %>% - left_join(nodes, by = c("from" = "id")) %>% - rename(x_from = x, y_from = y) %>% - select(-size) %>% - left_join(nodes, by = c("to" = "id")) %>% - rename(x_to = x, y_to = y) %>% - select(-size) - - list(nodes = nodes, edges = edges) -} - -# Create document stack data -create_document_data <- function() { - # Three stacked documents (left side of hex) - data.frame( - x = c(0.25, 0.23, 0.27), # Slightly offset x positions - y = c(0.65, 0.62, 0.68), # Slightly offset y positions - width = c(0.12, 0.12, 0.12), # Document width - height = c(0.15, 0.15, 0.15), # Document height - layer = c(1, 2, 3) # Stacking order - ) -} - -# Main function to create the hex sticker -create_putior_hex <- function() { - # Get data - hex_shape <- create_hexagon() - network_data <- create_network_data() - doc_data <- create_document_data() - - # Color palette - bg_color <- "#6B5B95" # Main purple - bg_light <- "#8B7BA5" # Lighter purple for gradient - border_color <- "#000000" # Black border - text_color <- "#FFFFFF" # White text - node_color <- "#FFFFFF" # White nodes - doc_color <- "#FFFFFF" # White documents - - # Create the plot - p <- ggplot() + - - # Background gradient effect (multiple layers for gradient simulation) - geom_polygon(data = hex_shape, - aes(x, y), - fill = bg_light, - color = NA, - alpha = 0.8) + - - geom_polygon(data = hex_shape, - aes(x, y), - fill = bg_color, - color = NA, - alpha = 0.6) + - - # Hexagon border - geom_polygon(data = hex_shape, - aes(x, y), - fill = NA, - color = border_color, - size = 1.5) + - - # Network edges - geom_segment(data = network_data$edges, - aes(x = x_from, y = y_from, - xend = x_to, yend = y_to), - color = text_color, - alpha = 0.4, - size = 0.8) + - - # Network nodes - geom_point(data = network_data$nodes, - aes(x, y, size = size), - color = node_color, - alpha = 0.9) + - - # Document stack (back to front) - geom_rect(data = arrange(doc_data, layer), - aes(xmin = x - width/2, xmax = x + width/2, - ymin = y - height/2, ymax = y + height/2), - fill = doc_color, - color = bg_color, - alpha = 0.95, - size = 0.5) + - - # Document text annotations - annotate("text", - x = doc_data$x[1], - y = doc_data$y[1] + 0.02, - label = "# put", - size = 3, - color = bg_color, - family = "mono", - fontface = "bold") + - - # Package name - annotate("text", - x = 0.5, - y = 0.15, - label = "putior", - size = 12, - color = text_color, - fontface = "bold") + - - # Styling - scale_size_identity() + - coord_fixed(ratio = 1) + - xlim(0, 1) + - ylim(0, 1) + - theme_void() + - theme( - plot.background = element_blank(), - panel.background = element_blank(), - plot.margin = margin(0, 0, 0, 0) - ) - - return(p) -} - -# Alternative version with enhanced gradient effect -create_putior_hex_enhanced <- function() { - # Similar to above but with more sophisticated gradient - hex_shape <- create_hexagon() - network_data <- create_network_data() - doc_data <- create_document_data() - - # Create multiple gradient layers - gradient_layers <- data.frame( - layer = 1:5, - alpha = c(0.2, 0.25, 0.3, 0.35, 0.4), - color = c("#8B7BA5", "#7B6B95", "#6B5B95", "#5B4B85", "#4B3B75") - ) - - p <- ggplot() - - # Add gradient layers - for(i in 1:nrow(gradient_layers)) { - p <- p + - geom_polygon(data = hex_shape, - aes(x, y), - fill = gradient_layers$color[i], - color = NA, - alpha = gradient_layers$alpha[i]) - } - - # Add remaining elements (same as basic version) - p <- p + - geom_polygon(data = hex_shape, aes(x, y), - fill = NA, color = "#000000", size = 1.5) + - # ... (add other elements) - - theme_void() + - coord_fixed() - - return(p) -} - -# Function to save the hex sticker -save_putior_hex <- function(plot, filename = "putior_hex", - width = 2, height = 2, dpi = 300) { - - # Save as PNG (for package use) - ggsave(paste0(filename, ".png"), plot, - width = width, height = height, dpi = dpi, - bg = "transparent") - - # Save as SVG (for scalability) - ggsave(paste0(filename, ".svg"), plot, - width = width, height = height, - bg = "transparent") - - cat("Hex sticker saved as:", paste0(filename, c(".png", ".svg")), "\n") -} - -# Usage example: -if (FALSE) { # Set to TRUE to run - # Create the hex sticker - hex_plot <- create_putior_hex() - - # Preview - print(hex_plot) - - # Save to files - save_putior_hex(hex_plot, "putior_hex_ggplot2") -} - -# Notes for further customization: -# 1. Adjust node positions in create_network_data() to match original exactly -# 2. Fine-tune colors to match original purple gradient -# 3. Add more sophisticated document shapes if needed -# 4. Consider using ggraph for more complex network layouts -# 5. Use showtext package for custom fonts if required +# Create putior hex sticker - Recommended ggplot2 approach +# This script recreates the putior hex sticker using pure ggplot2 + +library(ggplot2) +library(dplyr) + +# Function to create hexagon coordinates +create_hexagon <- function(center_x = 0.5, center_y = 0.5, size = 0.45) { + angles <- seq(30, 330, by = 60) * pi / 180 + data.frame( + x = center_x + size * cos(angles), + y = center_y + size * sin(angles) + ) +} + +# Create network data (mimicking the original design) +create_network_data <- function() { + set.seed(42) # For reproducibility + + # Node positions (right side of hex) + nodes <- data.frame( + x = c(0.65, 0.75, 0.85, 0.7, 0.8, 0.6, 0.9), + y = c(0.8, 0.7, 0.6, 0.5, 0.4, 0.6, 0.7), + size = c(4, 3.5, 3, 3.5, 3, 2.5, 2.5), + id = 1:7 + ) + + # Connections between nodes + edges <- data.frame( + from = c(1, 1, 2, 3, 4, 6), + to = c(2, 4, 3, 5, 5, 7) + ) %>% + left_join(nodes, by = c("from" = "id")) %>% + rename(x_from = x, y_from = y) %>% + select(-size) %>% + left_join(nodes, by = c("to" = "id")) %>% + rename(x_to = x, y_to = y) %>% + select(-size) + + list(nodes = nodes, edges = edges) +} + +# Create document stack data +create_document_data <- function() { + # Three stacked documents (left side of hex) + data.frame( + x = c(0.25, 0.23, 0.27), # Slightly offset x positions + y = c(0.65, 0.62, 0.68), # Slightly offset y positions + width = c(0.12, 0.12, 0.12), # Document width + height = c(0.15, 0.15, 0.15), # Document height + layer = c(1, 2, 3) # Stacking order + ) +} + +# Main function to create the hex sticker +create_putior_hex <- function() { + # Get data + hex_shape <- create_hexagon() + network_data <- create_network_data() + doc_data <- create_document_data() + + # Color palette + bg_color <- "#6B5B95" # Main purple + bg_light <- "#8B7BA5" # Lighter purple for gradient + border_color <- "#000000" # Black border + text_color <- "#FFFFFF" # White text + node_color <- "#FFFFFF" # White nodes + doc_color <- "#FFFFFF" # White documents + + # Create the plot + p <- ggplot() + + + # Background gradient effect (multiple layers for gradient simulation) + geom_polygon(data = hex_shape, + aes(x, y), + fill = bg_light, + color = NA, + alpha = 0.8) + + + geom_polygon(data = hex_shape, + aes(x, y), + fill = bg_color, + color = NA, + alpha = 0.6) + + + # Hexagon border + geom_polygon(data = hex_shape, + aes(x, y), + fill = NA, + color = border_color, + size = 1.5) + + + # Network edges + geom_segment(data = network_data$edges, + aes(x = x_from, y = y_from, + xend = x_to, yend = y_to), + color = text_color, + alpha = 0.4, + size = 0.8) + + + # Network nodes + geom_point(data = network_data$nodes, + aes(x, y, size = size), + color = node_color, + alpha = 0.9) + + + # Document stack (back to front) + geom_rect(data = arrange(doc_data, layer), + aes(xmin = x - width/2, xmax = x + width/2, + ymin = y - height/2, ymax = y + height/2), + fill = doc_color, + color = bg_color, + alpha = 0.95, + size = 0.5) + + + # Document text annotations + annotate("text", + x = doc_data$x[1], + y = doc_data$y[1] + 0.02, + label = "# put", + size = 3, + color = bg_color, + family = "mono", + fontface = "bold") + + + # Package name + annotate("text", + x = 0.5, + y = 0.15, + label = "putior", + size = 12, + color = text_color, + fontface = "bold") + + + # Styling + scale_size_identity() + + coord_fixed(ratio = 1) + + xlim(0, 1) + + ylim(0, 1) + + theme_void() + + theme( + plot.background = element_blank(), + panel.background = element_blank(), + plot.margin = margin(0, 0, 0, 0) + ) + + return(p) +} + +# Alternative version with enhanced gradient effect +create_putior_hex_enhanced <- function() { + # Similar to above but with more sophisticated gradient + hex_shape <- create_hexagon() + network_data <- create_network_data() + doc_data <- create_document_data() + + # Create multiple gradient layers + gradient_layers <- data.frame( + layer = 1:5, + alpha = c(0.2, 0.25, 0.3, 0.35, 0.4), + color = c("#8B7BA5", "#7B6B95", "#6B5B95", "#5B4B85", "#4B3B75") + ) + + p <- ggplot() + + # Add gradient layers + for(i in 1:nrow(gradient_layers)) { + p <- p + + geom_polygon(data = hex_shape, + aes(x, y), + fill = gradient_layers$color[i], + color = NA, + alpha = gradient_layers$alpha[i]) + } + + # Add remaining elements (same as basic version) + p <- p + + geom_polygon(data = hex_shape, aes(x, y), + fill = NA, color = "#000000", size = 1.5) + + # ... (add other elements) + + theme_void() + + coord_fixed() + + return(p) +} + +# Function to save the hex sticker +save_putior_hex <- function(plot, filename = "putior_hex", + width = 2, height = 2, dpi = 300) { + + # Save as PNG (for package use) + ggsave(paste0(filename, ".png"), plot, + width = width, height = height, dpi = dpi, + bg = "transparent") + + # Save as SVG (for scalability) + ggsave(paste0(filename, ".svg"), plot, + width = width, height = height, + bg = "transparent") + + cat("Hex sticker saved as:", paste0(filename, c(".png", ".svg")), "\n") +} + +# Usage example: +if (FALSE) { # Set to TRUE to run + # Create the hex sticker + hex_plot <- create_putior_hex() + + # Preview + print(hex_plot) + + # Save to files + save_putior_hex(hex_plot, "putior_hex_ggplot2") +} + +# Notes for further customization: +# 1. Adjust node positions in create_network_data() to match original exactly +# 2. Fine-tune colors to match original purple gradient +# 3. Add more sophisticated document shapes if needed +# 4. Consider using ggraph for more complex network layouts +# 5. Use showtext package for custom fonts if required # 6. Add drop shadow effects with additional polygon layers \ No newline at end of file diff --git a/inst/examples/data-science-workflow.R b/inst/examples/data-science-workflow.R index 596f565..eb80818 100644 --- a/inst/examples/data-science-workflow.R +++ b/inst/examples/data-science-workflow.R @@ -1,228 +1,228 @@ -# Data Science Workflow Example: Modular Functions with source() -# ============================================================================== -# This example demonstrates the most common data science pattern: modularizing -# functions into separate scripts and orchestrating them in a main workflow. -# -# To run this example: -# source(system.file("examples", "data-science-workflow.R", package = "putior")) -# ============================================================================== - -library(putior) - -cat("๐ฌ Data Science Workflow Example: Modular source() Pattern\n") -cat(paste(rep("=", 60), collapse = ""), "\n\n") - -# Create example directory structure -temp_dir <- file.path(tempdir(), "putior_datascience_example") -dir.create(temp_dir, showWarnings = FALSE) - -cat("๐ Creating modular data science workflow...\n") - -# utils.R - Utility functions -utils_script <- c( - "# Data utilities - sourced by main workflow", - "# put label:\"Data Utilities\", node_type:\"input\"", - "# output defaults to 'utils.R' - this script provides itself", - "", - "load_and_clean <- function(file_path) {", - " cat(\"Loading data from:\", file_path, \"\\n\")", - " # Simulate loading CSV data", - " data <- data.frame(", - " id = 1:100,", - " value = rnorm(100, 50, 10),", - " category = sample(c(\"A\", \"B\", \"C\"), 100, replace = TRUE),", - " date = seq.Date(as.Date(\"2024-01-01\"), by = \"day\", length.out = 100)", - " )", - " ", - " # Clean data", - " data <- data[complete.cases(data), ]", - " cat(\"Cleaned data:\", nrow(data), \"rows\\n\")", - " return(data)", - "}", - "", - "validate_data <- function(data) {", - " cat(\"Validating data quality...\\n\")", - " stopifnot(nrow(data) > 0)", - " stopifnot(all(c(\"id\", \"value\", \"category\") %in% names(data)))", - " cat(\"Data validation passed\\n\")", - " return(data)", - "}", - "", - "save_data <- function(data, file_path) {", - " write.csv(data, file_path, row.names = FALSE)", - " cat(\"Data saved to:\", file_path, \"\\n\")", - "}" -) - -# analysis.R - Analysis functions that depend on utils.R -analysis_script <- c( - "# Analysis functions - sourced by main workflow, depends on utils.R", - "# put label:\"Statistical Analysis\", node_type:\"process\", input:\"utils.R\"", - "# input: utils.R because we use utility functions", - "# output defaults to 'analysis.R'", - "", - "perform_descriptive_analysis <- function(data) {", - " cat(\"Performing descriptive analysis...\\n\")", - " # Uses validate_data() from utils.R", - " data <- validate_data(data)", - " ", - " summary_stats <- list(", - " mean_value = mean(data$value),", - " sd_value = sd(data$value),", - " n_categories = length(unique(data$category)),", - " date_range = range(data$date)", - " )", - " ", - " cat(\"Analysis complete:\\n\")", - " cat(\" Mean value:\", round(summary_stats$mean_value, 2), \"\\n\")", - " cat(\" Categories:\", summary_stats$n_categories, \"\\n\")", - " return(summary_stats)", - "}", - "", - "perform_category_analysis <- function(data) {", - " cat(\"Analyzing by category...\\n\")", - " # Group analysis", - " category_stats <- aggregate(data$value, by = list(data$category), FUN = mean)", - " names(category_stats) <- c(\"category\", \"mean_value\")", - " return(category_stats)", - "}" -) - -# visualization.R - Plotting functions that depend on analysis.R -visualization_script <- c( - "# Visualization functions - sourced by main workflow, depends on analysis.R", - "# put label:\"Data Visualization\", node_type:\"process\", input:\"analysis.R\"", - "# input: analysis.R because we use analysis results", - "# output defaults to 'visualization.R'", - "", - "create_summary_plot <- function(data, output_dir) {", - " cat(\"Creating summary visualizations...\\n\")", - " ", - " # Create simple plots", - " png(file.path(output_dir, \"value_histogram.png\"))", - " hist(data$value, main = \"Distribution of Values\", col = \"lightblue\")", - " dev.off()", - " ", - " png(file.path(output_dir, \"category_boxplot.png\"))", - " boxplot(value ~ category, data = data, main = \"Values by Category\")", - " dev.off()", - " ", - " cat(\"Plots saved to:\", output_dir, \"\\n\")", - "}", - "", - "create_analysis_report <- function(summary_stats, category_stats, output_file) {", - " cat(\"Generating analysis report...\\n\")", - " ", - " report <- c(", - " \"# Data Analysis Report\",", - " paste(\"Generated:\", Sys.time()),", - " \"\",", - " \"## Summary Statistics\",", - " paste(\"- Mean value:\", round(summary_stats$mean_value, 2)),", - " paste(\"- Standard deviation:\", round(summary_stats$sd_value, 2)),", - " paste(\"- Number of categories:\", summary_stats$n_categories),", - " \"\",", - " \"## Category Analysis\",", - " paste(capture.output(print(category_stats)), collapse = \"\\n\")", - " )", - " ", - " writeLines(report, output_file)", - " cat(\"Report saved to:\", output_file, \"\\n\")", - "}" -) - -# main.R - Main workflow orchestrator -main_script <- c( - "# Main data science workflow - orchestrates the entire analysis", - "# put label:\"Data Science Pipeline\", node_type:\"process\", input:\"utils.R,analysis.R,visualization.R\", output:\"analysis_report.md,plots/\"", - "", - "# Source all function modules", - "source(\"utils.R\") # Load data utility functions", - "source(\"analysis.R\") # Load statistical analysis functions", - "source(\"visualization.R\") # Load visualization functions", - "", - "cat(\"๐ Starting data science pipeline...\\n\\n\")", - "", - "# Step 1: Data loading and cleaning", - "cat(\"Step 1: Data Loading & Cleaning\\n\")", - "raw_data <- load_and_clean(\"input_data.csv\") # Uses utils.R", - "", - "# Step 2: Statistical analysis", - "cat(\"\\nStep 2: Statistical Analysis\\n\")", - "summary_stats <- perform_descriptive_analysis(raw_data) # Uses analysis.R", - "category_stats <- perform_category_analysis(raw_data) # Uses analysis.R", - "", - "# Step 3: Data visualization", - "cat(\"\\nStep 3: Data Visualization\\n\")", - "plots_dir <- \"plots\"", - "dir.create(plots_dir, showWarnings = FALSE)", - "create_summary_plot(raw_data, plots_dir) # Uses visualization.R", - "", - "# Step 4: Report generation", - "cat(\"\\nStep 4: Report Generation\\n\")", - "create_analysis_report(summary_stats, category_stats, \"analysis_report.md\") # Uses visualization.R", - "", - "cat(\"\\nโ Data science pipeline complete!\\n\")", - "cat(\"๐ Results available in: analysis_report.md and plots/\\n\")" -) - -# Write all files -writeLines(utils_script, file.path(temp_dir, "utils.R")) -writeLines(analysis_script, file.path(temp_dir, "analysis.R")) -writeLines(visualization_script, file.path(temp_dir, "visualization.R")) -writeLines(main_script, file.path(temp_dir, "main.R")) - -cat("โ Created modular data science workflow files\n\n") - -# Extract and visualize the workflow -cat("๐ Extracting modular workflow structure...\n") -workflow <- put(temp_dir) - -cat("\n๐ Workflow structure:\n") -cat(paste(rep("-", 55), collapse = ""), "\n") - -# Display the relationships -for (i in seq_len(nrow(workflow))) { - row <- workflow[i, ] - cat(sprintf("%-20s: %s\n", "File", row$file_name)) - cat(sprintf("%-20s: %s\n", "Label", row$label)) - cat(sprintf("%-20s: %s\n", "Type", ifelse(is.na(row$node_type), "process", row$node_type))) - cat(sprintf("%-20s: %s\n", "Dependencies", ifelse(is.na(row$input), "none", row$input))) - cat(sprintf("%-20s: %s\n", "Provides", row$output)) - cat(paste(rep("-", 55), collapse = ""), "\n") -} - -# Generate diagram -cat("\n๐จ Modular workflow diagram:\n") -cat(paste(rep("-", 55), collapse = ""), "\n") -put_diagram(workflow, - theme = "github", - show_files = TRUE, - title = "Data Science Modular Workflow") - -cat("\n\n๐ก Data Science Pattern Insights:\n") -cat(paste(rep("=", 55), collapse = ""), "\n") -cat("1. ๐ฆ MODULARITY: Functions organized by purpose (utils, analysis, viz)\n") -cat("2. ๐ DEPENDENCIES: Clear dependency chain (utils โ analysis โ viz โ main)\n") -cat("3. โป๏ธ REUSABILITY: Utility functions can be reused across projects\n") -cat("4. ๐ SOURCE FLOW: Scripts are sourced INTO the main workflow\n") -cat("5. ๐ DATA + CODE: Main script handles data, modules provide functions\n\n") - -cat("๐ Best Practices Demonstrated:\n") -cat("โข Separate concerns: data handling vs. analysis vs. visualization\n") -cat("โข Clear dependencies: each module declares what it needs\n") -cat("โข Function encapsulation: reusable, testable code modules\n") -cat("โข Orchestration: main script coordinates the entire pipeline\n\n") - -cat("๐๏ธ Example files created in:\n") -cat(" ", temp_dir, "\n\n") - -cat("๐ Try running the workflow:\n") -cat(" setwd(\"", temp_dir, "\")\n", sep = "") -cat(" source(\"main.R\")\n\n") - -cat("โ Data science workflow example complete!\n") - -# Clean up -cat("๐งน Cleaning up...\n") +# Data Science Workflow Example: Modular Functions with source() +# ============================================================================== +# This example demonstrates the most common data science pattern: modularizing +# functions into separate scripts and orchestrating them in a main workflow. +# +# To run this example: +# source(system.file("examples", "data-science-workflow.R", package = "putior")) +# ============================================================================== + +library(putior) + +cat("๐ฌ Data Science Workflow Example: Modular source() Pattern\n") +cat(paste(rep("=", 60), collapse = ""), "\n\n") + +# Create example directory structure +temp_dir <- file.path(tempdir(), "putior_datascience_example") +dir.create(temp_dir, showWarnings = FALSE) + +cat("๐ Creating modular data science workflow...\n") + +# utils.R - Utility functions +utils_script <- c( + "# Data utilities - sourced by main workflow", + "# put label:\"Data Utilities\", node_type:\"input\"", + "# output defaults to 'utils.R' - this script provides itself", + "", + "load_and_clean <- function(file_path) {", + " cat(\"Loading data from:\", file_path, \"\\n\")", + " # Simulate loading CSV data", + " data <- data.frame(", + " id = 1:100,", + " value = rnorm(100, 50, 10),", + " category = sample(c(\"A\", \"B\", \"C\"), 100, replace = TRUE),", + " date = seq.Date(as.Date(\"2024-01-01\"), by = \"day\", length.out = 100)", + " )", + " ", + " # Clean data", + " data <- data[complete.cases(data), ]", + " cat(\"Cleaned data:\", nrow(data), \"rows\\n\")", + " return(data)", + "}", + "", + "validate_data <- function(data) {", + " cat(\"Validating data quality...\\n\")", + " stopifnot(nrow(data) > 0)", + " stopifnot(all(c(\"id\", \"value\", \"category\") %in% names(data)))", + " cat(\"Data validation passed\\n\")", + " return(data)", + "}", + "", + "save_data <- function(data, file_path) {", + " write.csv(data, file_path, row.names = FALSE)", + " cat(\"Data saved to:\", file_path, \"\\n\")", + "}" +) + +# analysis.R - Analysis functions that depend on utils.R +analysis_script <- c( + "# Analysis functions - sourced by main workflow, depends on utils.R", + "# put label:\"Statistical Analysis\", node_type:\"process\", input:\"utils.R\"", + "# input: utils.R because we use utility functions", + "# output defaults to 'analysis.R'", + "", + "perform_descriptive_analysis <- function(data) {", + " cat(\"Performing descriptive analysis...\\n\")", + " # Uses validate_data() from utils.R", + " data <- validate_data(data)", + " ", + " summary_stats <- list(", + " mean_value = mean(data$value),", + " sd_value = sd(data$value),", + " n_categories = length(unique(data$category)),", + " date_range = range(data$date)", + " )", + " ", + " cat(\"Analysis complete:\\n\")", + " cat(\" Mean value:\", round(summary_stats$mean_value, 2), \"\\n\")", + " cat(\" Categories:\", summary_stats$n_categories, \"\\n\")", + " return(summary_stats)", + "}", + "", + "perform_category_analysis <- function(data) {", + " cat(\"Analyzing by category...\\n\")", + " # Group analysis", + " category_stats <- aggregate(data$value, by = list(data$category), FUN = mean)", + " names(category_stats) <- c(\"category\", \"mean_value\")", + " return(category_stats)", + "}" +) + +# visualization.R - Plotting functions that depend on analysis.R +visualization_script <- c( + "# Visualization functions - sourced by main workflow, depends on analysis.R", + "# put label:\"Data Visualization\", node_type:\"process\", input:\"analysis.R\"", + "# input: analysis.R because we use analysis results", + "# output defaults to 'visualization.R'", + "", + "create_summary_plot <- function(data, output_dir) {", + " cat(\"Creating summary visualizations...\\n\")", + " ", + " # Create simple plots", + " png(file.path(output_dir, \"value_histogram.png\"))", + " hist(data$value, main = \"Distribution of Values\", col = \"lightblue\")", + " dev.off()", + " ", + " png(file.path(output_dir, \"category_boxplot.png\"))", + " boxplot(value ~ category, data = data, main = \"Values by Category\")", + " dev.off()", + " ", + " cat(\"Plots saved to:\", output_dir, \"\\n\")", + "}", + "", + "create_analysis_report <- function(summary_stats, category_stats, output_file) {", + " cat(\"Generating analysis report...\\n\")", + " ", + " report <- c(", + " \"# Data Analysis Report\",", + " paste(\"Generated:\", Sys.time()),", + " \"\",", + " \"## Summary Statistics\",", + " paste(\"- Mean value:\", round(summary_stats$mean_value, 2)),", + " paste(\"- Standard deviation:\", round(summary_stats$sd_value, 2)),", + " paste(\"- Number of categories:\", summary_stats$n_categories),", + " \"\",", + " \"## Category Analysis\",", + " paste(capture.output(print(category_stats)), collapse = \"\\n\")", + " )", + " ", + " writeLines(report, output_file)", + " cat(\"Report saved to:\", output_file, \"\\n\")", + "}" +) + +# main.R - Main workflow orchestrator +main_script <- c( + "# Main data science workflow - orchestrates the entire analysis", + "# put label:\"Data Science Pipeline\", node_type:\"process\", input:\"utils.R,analysis.R,visualization.R\", output:\"analysis_report.md,plots/\"", + "", + "# Source all function modules", + "source(\"utils.R\") # Load data utility functions", + "source(\"analysis.R\") # Load statistical analysis functions", + "source(\"visualization.R\") # Load visualization functions", + "", + "cat(\"๐ Starting data science pipeline...\\n\\n\")", + "", + "# Step 1: Data loading and cleaning", + "cat(\"Step 1: Data Loading & Cleaning\\n\")", + "raw_data <- load_and_clean(\"input_data.csv\") # Uses utils.R", + "", + "# Step 2: Statistical analysis", + "cat(\"\\nStep 2: Statistical Analysis\\n\")", + "summary_stats <- perform_descriptive_analysis(raw_data) # Uses analysis.R", + "category_stats <- perform_category_analysis(raw_data) # Uses analysis.R", + "", + "# Step 3: Data visualization", + "cat(\"\\nStep 3: Data Visualization\\n\")", + "plots_dir <- \"plots\"", + "dir.create(plots_dir, showWarnings = FALSE)", + "create_summary_plot(raw_data, plots_dir) # Uses visualization.R", + "", + "# Step 4: Report generation", + "cat(\"\\nStep 4: Report Generation\\n\")", + "create_analysis_report(summary_stats, category_stats, \"analysis_report.md\") # Uses visualization.R", + "", + "cat(\"\\nโ Data science pipeline complete!\\n\")", + "cat(\"๐ Results available in: analysis_report.md and plots/\\n\")" +) + +# Write all files +writeLines(utils_script, file.path(temp_dir, "utils.R")) +writeLines(analysis_script, file.path(temp_dir, "analysis.R")) +writeLines(visualization_script, file.path(temp_dir, "visualization.R")) +writeLines(main_script, file.path(temp_dir, "main.R")) + +cat("โ Created modular data science workflow files\n\n") + +# Extract and visualize the workflow +cat("๐ Extracting modular workflow structure...\n") +workflow <- put(temp_dir) + +cat("\n๐ Workflow structure:\n") +cat(paste(rep("-", 55), collapse = ""), "\n") + +# Display the relationships +for (i in seq_len(nrow(workflow))) { + row <- workflow[i, ] + cat(sprintf("%-20s: %s\n", "File", row$file_name)) + cat(sprintf("%-20s: %s\n", "Label", row$label)) + cat(sprintf("%-20s: %s\n", "Type", ifelse(is.na(row$node_type), "process", row$node_type))) + cat(sprintf("%-20s: %s\n", "Dependencies", ifelse(is.na(row$input), "none", row$input))) + cat(sprintf("%-20s: %s\n", "Provides", row$output)) + cat(paste(rep("-", 55), collapse = ""), "\n") +} + +# Generate diagram +cat("\n๐จ Modular workflow diagram:\n") +cat(paste(rep("-", 55), collapse = ""), "\n") +put_diagram(workflow, + theme = "github", + show_files = TRUE, + title = "Data Science Modular Workflow") + +cat("\n\n๐ก Data Science Pattern Insights:\n") +cat(paste(rep("=", 55), collapse = ""), "\n") +cat("1. ๐ฆ MODULARITY: Functions organized by purpose (utils, analysis, viz)\n") +cat("2. ๐ DEPENDENCIES: Clear dependency chain (utils โ analysis โ viz โ main)\n") +cat("3. โป๏ธ REUSABILITY: Utility functions can be reused across projects\n") +cat("4. ๐ SOURCE FLOW: Scripts are sourced INTO the main workflow\n") +cat("5. ๐ DATA + CODE: Main script handles data, modules provide functions\n\n") + +cat("๐ Best Practices Demonstrated:\n") +cat("โข Separate concerns: data handling vs. analysis vs. visualization\n") +cat("โข Clear dependencies: each module declares what it needs\n") +cat("โข Function encapsulation: reusable, testable code modules\n") +cat("โข Orchestration: main script coordinates the entire pipeline\n\n") + +cat("๐๏ธ Example files created in:\n") +cat(" ", temp_dir, "\n\n") + +cat("๐ Try running the workflow:\n") +cat(" setwd(\"", temp_dir, "\")\n", sep = "") +cat(" source(\"main.R\")\n\n") + +cat("โ Data science workflow example complete!\n") + +# Clean up +cat("๐งน Cleaning up...\n") unlink(temp_dir, recursive = TRUE) \ No newline at end of file diff --git a/inst/examples/diagram-example.R b/inst/examples/diagram-example.R index f5270b0..7cf59d1 100644 --- a/inst/examples/diagram-example.R +++ b/inst/examples/diagram-example.R @@ -1,228 +1,228 @@ -# Putior Diagram Example: Workflow Visualization with Mermaid -# ============================================================================== -# This example demonstrates putior's visualization capabilities by creating -# a sample workflow and generating elegant mermaid diagrams. -# -# To run this example: -# source(system.file("examples", "diagram-example.R", package = "putior")) -# ============================================================================== - -library(putior) - -cat("๐จ Putior Diagram Example: Workflow Visualization\n") -cat(paste(rep("=", 50), collapse = ""), "\n\n") - -# Create a sample workflow for demonstration -temp_dir <- file.path(tempdir(), "putior_diagram_example") -dir.create(temp_dir, showWarnings = FALSE) - -cat("๐ Creating sample workflow files...\n") - -# File 1: Data Collection (Python) -data_collection <- c( - "# Data Collection from Multiple Sources", - "# put id:\"fetch_sales\", label:\"Fetch Sales Data\", node_type:\"input\", output:\"raw_sales.csv\"", - "# put id:\"fetch_customers\", label:\"Fetch Customer Data\", node_type:\"input\", output:\"raw_customers.csv\"", - "", - "import pandas as pd", - "import requests", - "", - "# Fetch sales data", - "sales_data = fetch_api_data('/sales')", - "sales_data.to_csv('raw_sales.csv')", - "", - "# Fetch customer data", - "customer_data = fetch_api_data('/customers')", - "customer_data.to_csv('raw_customers.csv')" -) - -# File 2: Data Processing (R) -data_processing <- c( - "# Data Cleaning and Integration", - "# put id:\"clean_sales\", label:\"Clean Sales Data\", node_type:\"process\", input:\"raw_sales.csv\", output:\"clean_sales.csv\"", - "# put id:\"clean_customers\", label:\"Clean Customer Data\", node_type:\"process\", input:\"raw_customers.csv\", output:\"clean_customers.csv\"", - "# put id:\"merge_data\", label:\"Merge Datasets\", node_type:\"process\", input:\"clean_sales.csv,clean_customers.csv\", output:\"merged_data.csv\"", - "", - "library(dplyr)", - "library(readr)", - "", - "# Clean sales data", - "sales <- read_csv('raw_sales.csv') %>%", - " filter(!is.na(amount)) %>%", - " mutate(date = as.Date(date))", - "write_csv(sales, 'clean_sales.csv')", - "", - "# Clean customer data", - "customers <- read_csv('raw_customers.csv') %>%", - " filter(!is.na(customer_id))", - "write_csv(customers, 'clean_customers.csv')", - "", - "# Merge datasets", - "merged <- sales %>%", - " left_join(customers, by = 'customer_id')", - "write_csv(merged, 'merged_data.csv')" -) - -# File 3: Analysis and Decision Making (R) -analysis <- c( - "# Statistical Analysis and Decision Making", - "# put id:\"analyze_trends\", label:\"Analyze Trends\", node_type:\"process\", input:\"merged_data.csv\", output:\"trend_analysis.rds\"", - "# put id:\"quality_check\", label:\"Data Quality Check\", node_type:\"decision\", input:\"merged_data.csv\", output:\"quality_report.json\"", - "# put id:\"generate_insights\", label:\"Generate Insights\", node_type:\"process\", input:\"trend_analysis.rds\", output:\"insights.rds\"", - "", - "library(dplyr)", - "", - "# Analyze trends", - "data <- read_csv('merged_data.csv')", - "trends <- analyze_time_series(data)", - "saveRDS(trends, 'trend_analysis.rds')", - "", - "# Quality check", - "quality <- check_data_quality(data)", - "write_json(quality, 'quality_report.json')", - "", - "# Generate insights", - "trends <- readRDS('trend_analysis.rds')", - "insights <- generate_business_insights(trends)", - "saveRDS(insights, 'insights.rds')" -) - -# File 4: Reporting (R) -reporting <- c( - "# Report Generation and Distribution", - "# put id:\"create_dashboard\", label:\"Create Dashboard\", node_type:\"output\", input:\"insights.rds\", output:\"dashboard.html\"", - "# put id:\"executive_summary\", label:\"Executive Summary\", node_type:\"output\", input:\"insights.rds\", output:\"executive_summary.pdf\"", - "# put id:\"data_export\", label:\"Export Data\", node_type:\"output\", input:\"merged_data.csv\", output:\"final_dataset.xlsx\"", - "", - "library(rmarkdown)", - "library(plotly)", - "", - "# Create interactive dashboard", - "insights <- readRDS('insights.rds')", - "render('dashboard_template.Rmd', output_file = 'dashboard.html')", - "", - "# Generate executive summary", - "render('executive_template.Rmd', output_file = 'executive_summary.pdf')", - "", - "# Export final dataset", - "data <- read_csv('merged_data.csv')", - "write_xlsx(data, 'final_dataset.xlsx')" -) - -# Write all files -writeLines(data_collection, file.path(temp_dir, "01_collect_data.py")) -writeLines(data_processing, file.path(temp_dir, "02_process_data.R")) -writeLines(analysis, file.path(temp_dir, "03_analyze_data.R")) -writeLines(reporting, file.path(temp_dir, "04_generate_reports.R")) - -cat("โ Created", length(list.files(temp_dir)), "workflow files\n\n") - -# Extract workflow using putior -cat("๐ Extracting workflow with putior...\n") -workflow <- put(temp_dir) - -cat("๐ Found", nrow(workflow), "workflow nodes:\n") -for (i in seq_len(nrow(workflow))) { - row <- workflow[i, ] - cat(" -", row$id, "(", row$node_type, "): ", row$label, "\n") -} - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐จ DIAGRAM EXAMPLES\n") -cat(paste(rep("=", 50), collapse = ""), "\n\n") - -# Example 1: Basic diagram -cat("๐ Example 1: Basic Top-Down Diagram\n") -cat(paste(rep("-", 40), collapse = ""), "\n") -put_diagram(workflow) - -cat("\n๐ Example 2: Horizontal Layout with File Labels\n") -cat(paste(rep("-", 40), collapse = ""), "\n") -put_diagram(workflow, - direction = "LR", - show_files = TRUE, - title = "Data Processing Pipeline") - -cat("\n๐ Example 3: Detailed Labels with Styling\n") -cat(paste(rep("-", 40), collapse = ""), "\n") -put_diagram(workflow, - node_labels = "both", - style_nodes = TRUE, - title = "Complete Workflow Analysis") - -cat("\n๐ Example 4: Process-Only View\n") -cat(paste(rep("-", 40), collapse = ""), "\n") -# Filter to show only process nodes -process_workflow <- workflow[workflow$node_type == "process", ] -put_diagram(process_workflow, - direction = "LR", - title = "Data Processing Steps") - -# Example 5: Save to file -cat("\n๐พ Example 5: Saving Diagram to File\n") -cat(paste(rep("-", 40), collapse = ""), "\n") -output_file <- file.path(temp_dir, "workflow_diagram.md") -put_diagram(workflow, - output = "file", - file = output_file, - title = "Sales Data Analysis Workflow", - show_files = TRUE, - style_nodes = TRUE) - -cat("๐ Diagram saved to:", output_file, "\n") -cat(" You can view this in any markdown viewer or GitHub!\n\n") - -# Show workflow statistics -cat("๐ Workflow Statistics:\n") -cat(" Total nodes:", nrow(workflow), "\n") -cat(" Input nodes:", sum(workflow$node_type == "input", na.rm = TRUE), "\n") -cat(" Process nodes:", sum(workflow$node_type == "process", na.rm = TRUE), "\n") -cat(" Output nodes:", sum(workflow$node_type == "output", na.rm = TRUE), "\n") -cat(" Decision nodes:", sum(workflow$node_type == "decision", na.rm = TRUE), "\n") - -# Show file flow -cat("\n๐ Data Flow:\n") -inputs <- unique(workflow$input[!is.na(workflow$input)]) -outputs <- unique(workflow$output[!is.na(workflow$output)]) -intermediate <- intersect(inputs, outputs) -external_inputs <- setdiff(inputs, outputs) -final_outputs <- setdiff(outputs, inputs) - -if (length(external_inputs) > 0) { - cat(" External inputs:", paste(external_inputs, collapse = ", "), "\n") -} -if (length(intermediate) > 0) { - cat(" Intermediate files:", paste(intermediate, collapse = ", "), "\n") -} -if (length(final_outputs) > 0) { - cat(" Final outputs:", paste(final_outputs, collapse = ", "), "\n") -} - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ฏ USAGE TIPS\n") -cat(paste(rep("=", 50), collapse = ""), "\n\n") - -cat("๐ก The generated mermaid diagrams can be used in:\n") -cat(" โข GitHub README files (native rendering)\n") -cat(" โข GitLab documentation\n") -cat(" โข Jupyter notebooks\n") -cat(" โข R Markdown documents\n") -cat(" โข Quarto documents\n") -cat(" โข Any markdown viewer that supports mermaid\n\n") - -cat("๐ง Customization options:\n") -cat(" โข direction: 'TD', 'LR', 'BT', 'RL'\n") -cat(" โข node_labels: 'name', 'label', 'both'\n") -cat(" โข show_files: TRUE/FALSE (show file names on arrows)\n") -cat(" โข style_nodes: TRUE/FALSE (color-code by node type)\n") -cat(" โข output: 'console', 'file', 'clipboard'\n\n") - -cat("๐ Copy any diagram above and paste it into:\n") -cat(" โข GitHub markdown files\n") -cat(" โข Mermaid live editor: https://mermaid.live\n") -cat(" โข VS Code with mermaid extension\n\n") - -cat("๐๏ธ Example files created in:\n") -cat(" ", temp_dir, "\n\n") - +# Putior Diagram Example: Workflow Visualization with Mermaid +# ============================================================================== +# This example demonstrates putior's visualization capabilities by creating +# a sample workflow and generating elegant mermaid diagrams. +# +# To run this example: +# source(system.file("examples", "diagram-example.R", package = "putior")) +# ============================================================================== + +library(putior) + +cat("๐จ Putior Diagram Example: Workflow Visualization\n") +cat(paste(rep("=", 50), collapse = ""), "\n\n") + +# Create a sample workflow for demonstration +temp_dir <- file.path(tempdir(), "putior_diagram_example") +dir.create(temp_dir, showWarnings = FALSE) + +cat("๐ Creating sample workflow files...\n") + +# File 1: Data Collection (Python) +data_collection <- c( + "# Data Collection from Multiple Sources", + "# put id:\"fetch_sales\", label:\"Fetch Sales Data\", node_type:\"input\", output:\"raw_sales.csv\"", + "# put id:\"fetch_customers\", label:\"Fetch Customer Data\", node_type:\"input\", output:\"raw_customers.csv\"", + "", + "import pandas as pd", + "import requests", + "", + "# Fetch sales data", + "sales_data = fetch_api_data('/sales')", + "sales_data.to_csv('raw_sales.csv')", + "", + "# Fetch customer data", + "customer_data = fetch_api_data('/customers')", + "customer_data.to_csv('raw_customers.csv')" +) + +# File 2: Data Processing (R) +data_processing <- c( + "# Data Cleaning and Integration", + "# put id:\"clean_sales\", label:\"Clean Sales Data\", node_type:\"process\", input:\"raw_sales.csv\", output:\"clean_sales.csv\"", + "# put id:\"clean_customers\", label:\"Clean Customer Data\", node_type:\"process\", input:\"raw_customers.csv\", output:\"clean_customers.csv\"", + "# put id:\"merge_data\", label:\"Merge Datasets\", node_type:\"process\", input:\"clean_sales.csv,clean_customers.csv\", output:\"merged_data.csv\"", + "", + "library(dplyr)", + "library(readr)", + "", + "# Clean sales data", + "sales <- read_csv('raw_sales.csv') %>%", + " filter(!is.na(amount)) %>%", + " mutate(date = as.Date(date))", + "write_csv(sales, 'clean_sales.csv')", + "", + "# Clean customer data", + "customers <- read_csv('raw_customers.csv') %>%", + " filter(!is.na(customer_id))", + "write_csv(customers, 'clean_customers.csv')", + "", + "# Merge datasets", + "merged <- sales %>%", + " left_join(customers, by = 'customer_id')", + "write_csv(merged, 'merged_data.csv')" +) + +# File 3: Analysis and Decision Making (R) +analysis <- c( + "# Statistical Analysis and Decision Making", + "# put id:\"analyze_trends\", label:\"Analyze Trends\", node_type:\"process\", input:\"merged_data.csv\", output:\"trend_analysis.rds\"", + "# put id:\"quality_check\", label:\"Data Quality Check\", node_type:\"decision\", input:\"merged_data.csv\", output:\"quality_report.json\"", + "# put id:\"generate_insights\", label:\"Generate Insights\", node_type:\"process\", input:\"trend_analysis.rds\", output:\"insights.rds\"", + "", + "library(dplyr)", + "", + "# Analyze trends", + "data <- read_csv('merged_data.csv')", + "trends <- analyze_time_series(data)", + "saveRDS(trends, 'trend_analysis.rds')", + "", + "# Quality check", + "quality <- check_data_quality(data)", + "write_json(quality, 'quality_report.json')", + "", + "# Generate insights", + "trends <- readRDS('trend_analysis.rds')", + "insights <- generate_business_insights(trends)", + "saveRDS(insights, 'insights.rds')" +) + +# File 4: Reporting (R) +reporting <- c( + "# Report Generation and Distribution", + "# put id:\"create_dashboard\", label:\"Create Dashboard\", node_type:\"output\", input:\"insights.rds\", output:\"dashboard.html\"", + "# put id:\"executive_summary\", label:\"Executive Summary\", node_type:\"output\", input:\"insights.rds\", output:\"executive_summary.pdf\"", + "# put id:\"data_export\", label:\"Export Data\", node_type:\"output\", input:\"merged_data.csv\", output:\"final_dataset.xlsx\"", + "", + "library(rmarkdown)", + "library(plotly)", + "", + "# Create interactive dashboard", + "insights <- readRDS('insights.rds')", + "render('dashboard_template.Rmd', output_file = 'dashboard.html')", + "", + "# Generate executive summary", + "render('executive_template.Rmd', output_file = 'executive_summary.pdf')", + "", + "# Export final dataset", + "data <- read_csv('merged_data.csv')", + "write_xlsx(data, 'final_dataset.xlsx')" +) + +# Write all files +writeLines(data_collection, file.path(temp_dir, "01_collect_data.py")) +writeLines(data_processing, file.path(temp_dir, "02_process_data.R")) +writeLines(analysis, file.path(temp_dir, "03_analyze_data.R")) +writeLines(reporting, file.path(temp_dir, "04_generate_reports.R")) + +cat("โ Created", length(list.files(temp_dir)), "workflow files\n\n") + +# Extract workflow using putior +cat("๐ Extracting workflow with putior...\n") +workflow <- put(temp_dir) + +cat("๐ Found", nrow(workflow), "workflow nodes:\n") +for (i in seq_len(nrow(workflow))) { + row <- workflow[i, ] + cat(" -", row$id, "(", row$node_type, "): ", row$label, "\n") +} + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐จ DIAGRAM EXAMPLES\n") +cat(paste(rep("=", 50), collapse = ""), "\n\n") + +# Example 1: Basic diagram +cat("๐ Example 1: Basic Top-Down Diagram\n") +cat(paste(rep("-", 40), collapse = ""), "\n") +put_diagram(workflow) + +cat("\n๐ Example 2: Horizontal Layout with File Labels\n") +cat(paste(rep("-", 40), collapse = ""), "\n") +put_diagram(workflow, + direction = "LR", + show_files = TRUE, + title = "Data Processing Pipeline") + +cat("\n๐ Example 3: Detailed Labels with Styling\n") +cat(paste(rep("-", 40), collapse = ""), "\n") +put_diagram(workflow, + node_labels = "both", + style_nodes = TRUE, + title = "Complete Workflow Analysis") + +cat("\n๐ Example 4: Process-Only View\n") +cat(paste(rep("-", 40), collapse = ""), "\n") +# Filter to show only process nodes +process_workflow <- workflow[workflow$node_type == "process", ] +put_diagram(process_workflow, + direction = "LR", + title = "Data Processing Steps") + +# Example 5: Save to file +cat("\n๐พ Example 5: Saving Diagram to File\n") +cat(paste(rep("-", 40), collapse = ""), "\n") +output_file <- file.path(temp_dir, "workflow_diagram.md") +put_diagram(workflow, + output = "file", + file = output_file, + title = "Sales Data Analysis Workflow", + show_files = TRUE, + style_nodes = TRUE) + +cat("๐ Diagram saved to:", output_file, "\n") +cat(" You can view this in any markdown viewer or GitHub!\n\n") + +# Show workflow statistics +cat("๐ Workflow Statistics:\n") +cat(" Total nodes:", nrow(workflow), "\n") +cat(" Input nodes:", sum(workflow$node_type == "input", na.rm = TRUE), "\n") +cat(" Process nodes:", sum(workflow$node_type == "process", na.rm = TRUE), "\n") +cat(" Output nodes:", sum(workflow$node_type == "output", na.rm = TRUE), "\n") +cat(" Decision nodes:", sum(workflow$node_type == "decision", na.rm = TRUE), "\n") + +# Show file flow +cat("\n๐ Data Flow:\n") +inputs <- unique(workflow$input[!is.na(workflow$input)]) +outputs <- unique(workflow$output[!is.na(workflow$output)]) +intermediate <- intersect(inputs, outputs) +external_inputs <- setdiff(inputs, outputs) +final_outputs <- setdiff(outputs, inputs) + +if (length(external_inputs) > 0) { + cat(" External inputs:", paste(external_inputs, collapse = ", "), "\n") +} +if (length(intermediate) > 0) { + cat(" Intermediate files:", paste(intermediate, collapse = ", "), "\n") +} +if (length(final_outputs) > 0) { + cat(" Final outputs:", paste(final_outputs, collapse = ", "), "\n") +} + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ฏ USAGE TIPS\n") +cat(paste(rep("=", 50), collapse = ""), "\n\n") + +cat("๐ก The generated mermaid diagrams can be used in:\n") +cat(" โข GitHub README files (native rendering)\n") +cat(" โข GitLab documentation\n") +cat(" โข Jupyter notebooks\n") +cat(" โข R Markdown documents\n") +cat(" โข Quarto documents\n") +cat(" โข Any markdown viewer that supports mermaid\n\n") + +cat("๐ง Customization options:\n") +cat(" โข direction: 'TD', 'LR', 'BT', 'RL'\n") +cat(" โข node_labels: 'name', 'label', 'both'\n") +cat(" โข show_files: TRUE/FALSE (show file names on arrows)\n") +cat(" โข style_nodes: TRUE/FALSE (color-code by node type)\n") +cat(" โข output: 'console', 'file', 'clipboard'\n\n") + +cat("๐ Copy any diagram above and paste it into:\n") +cat(" โข GitHub markdown files\n") +cat(" โข Mermaid live editor: https://mermaid.live\n") +cat(" โข VS Code with mermaid extension\n\n") + +cat("๐๏ธ Example files created in:\n") +cat(" ", temp_dir, "\n\n") + cat("๐จ Happy workflow visualization with putior! ๐\n") \ No newline at end of file diff --git a/inst/examples/generate-readme-examples.R b/inst/examples/generate-readme-examples.R index 632cffe..0b853c5 100644 --- a/inst/examples/generate-readme-examples.R +++ b/inst/examples/generate-readme-examples.R @@ -1,183 +1,183 @@ -#!/usr/bin/env Rscript -# Generate README.md examples using putior itself -# This ensures examples always show current themes and styling - -library(putior) - -cat("๐ง Generating README examples with putior...\n") - -# ============================================================================= -# Example 1: Basic Quick Start Workflow -# ============================================================================= -cat("๐ Generating basic workflow example...\n") - -basic_workflow <- data.frame( - file_name = c("01_fetch_data.R", "02_clean_data.py"), - id = c("fetch_sales", "clean_data"), - label = c("Fetch Sales Data", "Clean and Process"), - node_type = c("input", "process"), - input = c(NA, "sales_data.csv"), - output = c("sales_data.csv", "clean_sales.csv"), - stringsAsFactors = FALSE -) - -basic_example <- put_diagram(basic_workflow, theme = "github", output = "none") - -# ============================================================================= -# Example 2: Workflow Boundaries Demo -# ============================================================================= -cat("โก Generating workflow boundaries examples...\n") - -boundary_workflow <- data.frame( - file_name = c("start.R", "extract.R", "transform.R", "end.R"), - id = c("pipeline_start", "extract_data", "transform_data", "pipeline_end"), - label = c("Data Pipeline Start", "Extract Raw Data", "Transform Data", "Pipeline Complete"), - node_type = c("start", "process", "process", "end"), - input = c(NA, "raw_config.json", "raw_data.csv", "clean_data.csv"), - output = c("raw_config.json", "raw_data.csv", "clean_data.csv", NA), - stringsAsFactors = FALSE -) - -# With boundaries (default) -boundary_with <- put_diagram(boundary_workflow, - show_workflow_boundaries = TRUE, - theme = "github", - output = "none") - -# Without boundaries -boundary_without <- put_diagram(boundary_workflow, - show_workflow_boundaries = FALSE, - theme = "github", - output = "none") - -# ============================================================================= -# Example 3: Data Science Pipeline -# ============================================================================= -cat("๐ Generating data science pipeline example...\n") - -datascience_workflow <- data.frame( - file_name = c("01_fetch_sales.R", "02_fetch_customers.R", "03_clean_sales.py", - "04_merge_data.R", "05_analyze.py", "06_report.R"), - id = c("fetch_sales", "fetch_customers", "clean_sales", "merge_data", "analyze", "report"), - label = c("Fetch Sales Data", "Fetch Customer Data", "Clean Sales Data", - "Merge Datasets", "Statistical Analysis", "Generate Final Report"), - node_type = c("input", "input", "process", "process", "process", "output"), - input = c(NA, NA, "sales_data.csv", "customers.csv,clean_sales.csv", "merged_data.csv", "analysis_results.csv"), - output = c("sales_data.csv", "customers.csv", "clean_sales.csv", "merged_data.csv", "analysis_results.csv", "final_report.pdf"), - stringsAsFactors = FALSE -) - -datascience_example <- put_diagram(datascience_workflow, theme = "github", output = "none") - -# ============================================================================= -# Example 4: Theme Showcase -# ============================================================================= -cat("๐จ Generating theme examples...\n") - -theme_workflow <- data.frame( - file_name = c("fetch.R", "process.R", "report.R"), - id = c("fetch_data", "clean_data", "generate_report"), - label = c("Fetch API Data", "Clean and Validate", "Generate Final Report"), - node_type = c("input", "process", "output"), - input = c(NA, "raw_data.csv", "clean_data.csv"), - output = c("raw_data.csv", "clean_data.csv", "final_report.html"), - stringsAsFactors = FALSE -) - -# Generate all theme examples -themes <- c("light", "dark", "auto", "github", "minimal") -theme_examples <- list() - -for (theme in themes) { - cat(" ๐จ Generating", theme, "theme...\n") - theme_examples[[theme]] <- put_diagram(theme_workflow, - theme = theme, - output = "none") -} - -# ============================================================================= -# Example 5: Modular Source Workflow -# ============================================================================= -cat("๐ฆ Generating modular source workflow...\n") - -modular_workflow <- data.frame( - file_name = c("utils.R", "analysis.R", "main.R"), - id = c("utils", "analysis", "main"), - label = c("Data Utilities", "Statistical Analysis", "Main Analysis Pipeline"), - node_type = c("input", "process", "process"), - input = c(NA, "utils.R", "utils.R,analysis.R"), - output = c("utils.R", "analysis.R", "results.csv"), - stringsAsFactors = FALSE -) - -# Simple mode -modular_simple <- put_diagram(modular_workflow, theme = "github", output = "none") - -# Artifact mode -modular_artifacts <- put_diagram(modular_workflow, - show_artifacts = TRUE, - theme = "github", - output = "none") - -# ============================================================================= -# Example 6: Simple vs Artifact Comparison -# ============================================================================= -cat("๐ Generating simple vs artifact comparison...\n") - -comparison_workflow <- data.frame( - file_name = c("load.R", "process.R", "analyze.R"), - id = c("load", "process", "analyze"), - label = c("Load Data", "Process Data", "Analyze"), - node_type = c("input", "process", "output"), - input = c(NA, "raw_data.csv", "clean_data.csv"), - output = c("raw_data.csv", "clean_data.csv", "results.json"), - stringsAsFactors = FALSE -) - -# Simple mode -comparison_simple <- put_diagram(comparison_workflow, - show_artifacts = FALSE, - theme = "github", - output = "none") - -# Artifact mode -comparison_artifacts <- put_diagram(comparison_workflow, - show_artifacts = TRUE, - theme = "github", - output = "none") - -# ============================================================================= -# Output Results -# ============================================================================= -cat("\nโ Generated all examples!\n\n") - -cat("๐ Example outputs:\n") -cat("==================\n") - -examples <- list( - "Basic Workflow" = basic_example, - "Boundaries Enabled" = boundary_with, - "Boundaries Disabled" = boundary_without, - "Data Science Pipeline" = datascience_example, - "Modular Simple" = modular_simple, - "Modular Artifacts" = modular_artifacts, - "Comparison Simple" = comparison_simple, - "Comparison Artifacts" = comparison_artifacts -) - -# Add theme examples -for (theme in themes) { - examples[[paste("Theme:", theme)]] <- theme_examples[[theme]] -} - -# Save examples to a structured list for potential README integration -saveRDS(examples, "readme_examples.rds") - -cat("๐พ Saved examples to readme_examples.rds\n") -cat("๐ฏ Ready for README integration!\n") - -# Print first example as demonstration -cat("\n๐ Sample output (Basic Workflow):\n") -cat("```mermaid\n") -cat(basic_example) +#!/usr/bin/env Rscript +# Generate README.md examples using putior itself +# This ensures examples always show current themes and styling + +library(putior) + +cat("๐ง Generating README examples with putior...\n") + +# ============================================================================= +# Example 1: Basic Quick Start Workflow +# ============================================================================= +cat("๐ Generating basic workflow example...\n") + +basic_workflow <- data.frame( + file_name = c("01_fetch_data.R", "02_clean_data.py"), + id = c("fetch_sales", "clean_data"), + label = c("Fetch Sales Data", "Clean and Process"), + node_type = c("input", "process"), + input = c(NA, "sales_data.csv"), + output = c("sales_data.csv", "clean_sales.csv"), + stringsAsFactors = FALSE +) + +basic_example <- put_diagram(basic_workflow, theme = "github", output = "none") + +# ============================================================================= +# Example 2: Workflow Boundaries Demo +# ============================================================================= +cat("โก Generating workflow boundaries examples...\n") + +boundary_workflow <- data.frame( + file_name = c("start.R", "extract.R", "transform.R", "end.R"), + id = c("pipeline_start", "extract_data", "transform_data", "pipeline_end"), + label = c("Data Pipeline Start", "Extract Raw Data", "Transform Data", "Pipeline Complete"), + node_type = c("start", "process", "process", "end"), + input = c(NA, "raw_config.json", "raw_data.csv", "clean_data.csv"), + output = c("raw_config.json", "raw_data.csv", "clean_data.csv", NA), + stringsAsFactors = FALSE +) + +# With boundaries (default) +boundary_with <- put_diagram(boundary_workflow, + show_workflow_boundaries = TRUE, + theme = "github", + output = "none") + +# Without boundaries +boundary_without <- put_diagram(boundary_workflow, + show_workflow_boundaries = FALSE, + theme = "github", + output = "none") + +# ============================================================================= +# Example 3: Data Science Pipeline +# ============================================================================= +cat("๐ Generating data science pipeline example...\n") + +datascience_workflow <- data.frame( + file_name = c("01_fetch_sales.R", "02_fetch_customers.R", "03_clean_sales.py", + "04_merge_data.R", "05_analyze.py", "06_report.R"), + id = c("fetch_sales", "fetch_customers", "clean_sales", "merge_data", "analyze", "report"), + label = c("Fetch Sales Data", "Fetch Customer Data", "Clean Sales Data", + "Merge Datasets", "Statistical Analysis", "Generate Final Report"), + node_type = c("input", "input", "process", "process", "process", "output"), + input = c(NA, NA, "sales_data.csv", "customers.csv,clean_sales.csv", "merged_data.csv", "analysis_results.csv"), + output = c("sales_data.csv", "customers.csv", "clean_sales.csv", "merged_data.csv", "analysis_results.csv", "final_report.pdf"), + stringsAsFactors = FALSE +) + +datascience_example <- put_diagram(datascience_workflow, theme = "github", output = "none") + +# ============================================================================= +# Example 4: Theme Showcase +# ============================================================================= +cat("๐จ Generating theme examples...\n") + +theme_workflow <- data.frame( + file_name = c("fetch.R", "process.R", "report.R"), + id = c("fetch_data", "clean_data", "generate_report"), + label = c("Fetch API Data", "Clean and Validate", "Generate Final Report"), + node_type = c("input", "process", "output"), + input = c(NA, "raw_data.csv", "clean_data.csv"), + output = c("raw_data.csv", "clean_data.csv", "final_report.html"), + stringsAsFactors = FALSE +) + +# Generate all theme examples +themes <- c("light", "dark", "auto", "github", "minimal") +theme_examples <- list() + +for (theme in themes) { + cat(" ๐จ Generating", theme, "theme...\n") + theme_examples[[theme]] <- put_diagram(theme_workflow, + theme = theme, + output = "none") +} + +# ============================================================================= +# Example 5: Modular Source Workflow +# ============================================================================= +cat("๐ฆ Generating modular source workflow...\n") + +modular_workflow <- data.frame( + file_name = c("utils.R", "analysis.R", "main.R"), + id = c("utils", "analysis", "main"), + label = c("Data Utilities", "Statistical Analysis", "Main Analysis Pipeline"), + node_type = c("input", "process", "process"), + input = c(NA, "utils.R", "utils.R,analysis.R"), + output = c("utils.R", "analysis.R", "results.csv"), + stringsAsFactors = FALSE +) + +# Simple mode +modular_simple <- put_diagram(modular_workflow, theme = "github", output = "none") + +# Artifact mode +modular_artifacts <- put_diagram(modular_workflow, + show_artifacts = TRUE, + theme = "github", + output = "none") + +# ============================================================================= +# Example 6: Simple vs Artifact Comparison +# ============================================================================= +cat("๐ Generating simple vs artifact comparison...\n") + +comparison_workflow <- data.frame( + file_name = c("load.R", "process.R", "analyze.R"), + id = c("load", "process", "analyze"), + label = c("Load Data", "Process Data", "Analyze"), + node_type = c("input", "process", "output"), + input = c(NA, "raw_data.csv", "clean_data.csv"), + output = c("raw_data.csv", "clean_data.csv", "results.json"), + stringsAsFactors = FALSE +) + +# Simple mode +comparison_simple <- put_diagram(comparison_workflow, + show_artifacts = FALSE, + theme = "github", + output = "none") + +# Artifact mode +comparison_artifacts <- put_diagram(comparison_workflow, + show_artifacts = TRUE, + theme = "github", + output = "none") + +# ============================================================================= +# Output Results +# ============================================================================= +cat("\nโ Generated all examples!\n\n") + +cat("๐ Example outputs:\n") +cat("==================\n") + +examples <- list( + "Basic Workflow" = basic_example, + "Boundaries Enabled" = boundary_with, + "Boundaries Disabled" = boundary_without, + "Data Science Pipeline" = datascience_example, + "Modular Simple" = modular_simple, + "Modular Artifacts" = modular_artifacts, + "Comparison Simple" = comparison_simple, + "Comparison Artifacts" = comparison_artifacts +) + +# Add theme examples +for (theme in themes) { + examples[[paste("Theme:", theme)]] <- theme_examples[[theme]] +} + +# Save examples to a structured list for potential README integration +saveRDS(examples, "readme_examples.rds") + +cat("๐พ Saved examples to readme_examples.rds\n") +cat("๐ฏ Ready for README integration!\n") + +# Print first example as demonstration +cat("\n๐ Sample output (Basic Workflow):\n") +cat("```mermaid\n") +cat(basic_example) cat("\n```\n") \ No newline at end of file diff --git a/inst/examples/hex-sticker-research.R b/inst/examples/hex-sticker-research.R index bb31d8d..4b87d58 100644 --- a/inst/examples/hex-sticker-research.R +++ b/inst/examples/hex-sticker-research.R @@ -1,199 +1,199 @@ -# Research on creating reproducible hex stickers for putior -# Comparing different approaches for creating complex hex stickers - -# Required libraries -library(ggplot2) -library(grid) -library(ggraph) -library(igraph) -library(showtext) - -# Approach 1: hexSticker package (requires installation) -# The hexSticker package is the standard tool for creating hex stickers -# Pros: -# - Purpose-built for hex stickers -# - Handles fonts, borders, and positioning automatically -# - Supports both ggplot2 plots and images as input -# - Built-in hexagon shape -# Cons: -# - Limited customization for complex designs -# - May need workarounds for multiple elements -# - Package not currently installed - -# Example structure (would work if hexSticker was installed): -# library(hexSticker) -# # Create a base plot with network visualization -# g <- make_graph(~ 1-2-3-4-5, 1-3, 1-4, 2-5) -# p <- ggraph(g, layout = 'fr') + -# geom_edge_link(color = "white", alpha = 0.5) + -# geom_node_point(color = "white", size = 3) + -# theme_void() -# -# sticker(p, -# package = "putior", -# p_size = 20, -# s_x = 1, -# s_y = 0.8, -# s_width = 1.3, -# s_height = 1, -# h_fill = "#6B5B95", -# h_color = "#000000", -# filename = "hex_hexsticker.png") - -# Approach 2: Pure ggplot2 with custom hexagon -# Pros: -# - Full control over every element -# - No additional dependencies beyond ggplot2 -# - Can layer multiple elements easily -# - Supports gradients via scale_fill_gradient -# Cons: -# - More code required -# - Need to manually create hexagon shape -# - Font handling more complex - -create_hexagon <- function(center_x = 0, center_y = 0, size = 1) { - angles <- seq(30, 330, by = 60) * pi / 180 - data.frame( - x = center_x + size * cos(angles), - y = center_y + size * sin(angles) - ) -} - -# Create network data -set.seed(42) -nodes <- data.frame( - x = c(0.3, 0.7, 0.5, 0.2, 0.8, 0.4, 0.6), - y = c(0.7, 0.7, 0.5, 0.3, 0.3, 0.4, 0.6), - size = c(3, 3, 2, 2, 2, 1.5, 1.5) -) - -edges <- data.frame( - x = c(0.3, 0.3, 0.7, 0.5, 0.2, 0.4), - y = c(0.7, 0.7, 0.7, 0.5, 0.3, 0.4), - xend = c(0.5, 0.2, 0.8, 0.4, 0.4, 0.6), - yend = c(0.5, 0.3, 0.3, 0.4, 0.4, 0.6) -) - -# Create document icons data -docs <- data.frame( - x = c(0.15, 0.12, 0.18), - y = c(0.75, 0.72, 0.78), - label = c("# put", "# put", "# put"), - size = c(4, 3.5, 3) -) - -# Create ggplot2 hex sticker -hex_ggplot <- ggplot() + - # Hexagon background with gradient effect - geom_polygon(data = create_hexagon(0.5, 0.5, 0.48), - aes(x, y), fill = "#6B5B95", color = NA) + - geom_polygon(data = create_hexagon(0.5, 0.5, 0.5), - aes(x, y), fill = NA, color = "black", size = 2) + - # Network edges - geom_segment(data = edges, - aes(x = x, y = y, xend = xend, yend = yend), - color = "white", alpha = 0.3, size = 0.5) + - # Network nodes - geom_point(data = nodes, - aes(x, y, size = size), - color = "white", alpha = 0.8) + - # Document icons (simplified as rectangles with text) - geom_rect(data = docs, - aes(xmin = x - 0.03, xmax = x + 0.03, - ymin = y - 0.02, ymax = y + 0.02), - fill = "white", alpha = 0.9) + - geom_text(data = docs, - aes(x, y, label = label, size = size), - color = "#6B5B95", family = "mono") + - # Package name - annotate("text", x = 0.5, y = 0.1, label = "putior", - size = 8, color = "white", fontface = "bold") + - # Styling - scale_size_identity() + - coord_fixed() + - theme_void() + - theme(plot.margin = margin(0, 0, 0, 0)) - -# Approach 3: Grid graphics -# Pros: -# - Low-level control -# - Can create exact shapes and gradients -# - Efficient for complex custom graphics -# Cons: -# - Steeper learning curve -# - More verbose code -# - Less integration with data visualization tools - -create_hex_grid <- function() { - grid.newpage() - - # Define hexagon vertices - angles <- seq(30, 330, by = 60) * pi / 180 - hex_x <- 0.5 + 0.4 * cos(angles) - hex_y <- 0.5 + 0.4 * sin(angles) - - # Create gradient background - gradient <- linearGradient( - colours = c("#4B4B6B", "#6B5B95", "#8B7BA5"), - x1 = 0, y1 = 1, x2 = 0, y2 = 0 - ) - - # Draw hexagon with gradient - grid.polygon(x = hex_x, y = hex_y, - gp = gpar(fill = "#6B5B95", col = "black", lwd = 3)) - - # Add network elements - # ... (would add circles, lines, text using grid functions) - - # Add text - grid.text("putior", x = 0.5, y = 0.1, - gp = gpar(col = "white", fontsize = 24, fontface = "bold")) -} - -# Approach 4: Hybrid approach - ggplot2 + hexSticker -# Pros: -# - Best of both worlds -# - Create complex plot in ggplot2, use hexSticker for final formatting -# - Handles fonts and borders well -# Cons: -# - Requires hexSticker package - -# Approach 5: Using additional packages -# - ggraph: Excellent for network visualizations -# - patchwork: Combine multiple plots -# - ggforce: Additional geoms for complex shapes -# - gganimate: For animated hex stickers - -# Recommendation for putior hex sticker: -# -# Given the requirements: -# 1. Multiple document/file icons -# 2. Network/graph visualization -# 3. Gradient background -# 4. Text elements -# -# Best approach: Pure ggplot2 or ggplot2 + hexSticker -# -# Pure ggplot2 advantages: -# - No additional dependencies -# - Full control over all elements -# - Can create gradient effects with multiple polygons -# - Easy to maintain and modify -# -# Implementation strategy: -# 1. Create hexagon shape as polygon -# 2. Layer gradient effect with multiple semi-transparent polygons -# 3. Use ggraph or manual positioning for network elements -# 4. Add document shapes as rectangles or custom polygons -# 5. Use annotate() or geom_text() for text elements -# 6. Export with ggsave() at appropriate resolution - -# Example implementation starter: -create_putior_hex <- function() { - # This would be the full implementation - # combining all elements shown above - # with proper styling and positioning -} - -# Save examples +# Research on creating reproducible hex stickers for putior +# Comparing different approaches for creating complex hex stickers + +# Required libraries +library(ggplot2) +library(grid) +library(ggraph) +library(igraph) +library(showtext) + +# Approach 1: hexSticker package (requires installation) +# The hexSticker package is the standard tool for creating hex stickers +# Pros: +# - Purpose-built for hex stickers +# - Handles fonts, borders, and positioning automatically +# - Supports both ggplot2 plots and images as input +# - Built-in hexagon shape +# Cons: +# - Limited customization for complex designs +# - May need workarounds for multiple elements +# - Package not currently installed + +# Example structure (would work if hexSticker was installed): +# library(hexSticker) +# # Create a base plot with network visualization +# g <- make_graph(~ 1-2-3-4-5, 1-3, 1-4, 2-5) +# p <- ggraph(g, layout = 'fr') + +# geom_edge_link(color = "white", alpha = 0.5) + +# geom_node_point(color = "white", size = 3) + +# theme_void() +# +# sticker(p, +# package = "putior", +# p_size = 20, +# s_x = 1, +# s_y = 0.8, +# s_width = 1.3, +# s_height = 1, +# h_fill = "#6B5B95", +# h_color = "#000000", +# filename = "hex_hexsticker.png") + +# Approach 2: Pure ggplot2 with custom hexagon +# Pros: +# - Full control over every element +# - No additional dependencies beyond ggplot2 +# - Can layer multiple elements easily +# - Supports gradients via scale_fill_gradient +# Cons: +# - More code required +# - Need to manually create hexagon shape +# - Font handling more complex + +create_hexagon <- function(center_x = 0, center_y = 0, size = 1) { + angles <- seq(30, 330, by = 60) * pi / 180 + data.frame( + x = center_x + size * cos(angles), + y = center_y + size * sin(angles) + ) +} + +# Create network data +set.seed(42) +nodes <- data.frame( + x = c(0.3, 0.7, 0.5, 0.2, 0.8, 0.4, 0.6), + y = c(0.7, 0.7, 0.5, 0.3, 0.3, 0.4, 0.6), + size = c(3, 3, 2, 2, 2, 1.5, 1.5) +) + +edges <- data.frame( + x = c(0.3, 0.3, 0.7, 0.5, 0.2, 0.4), + y = c(0.7, 0.7, 0.7, 0.5, 0.3, 0.4), + xend = c(0.5, 0.2, 0.8, 0.4, 0.4, 0.6), + yend = c(0.5, 0.3, 0.3, 0.4, 0.4, 0.6) +) + +# Create document icons data +docs <- data.frame( + x = c(0.15, 0.12, 0.18), + y = c(0.75, 0.72, 0.78), + label = c("# put", "# put", "# put"), + size = c(4, 3.5, 3) +) + +# Create ggplot2 hex sticker +hex_ggplot <- ggplot() + + # Hexagon background with gradient effect + geom_polygon(data = create_hexagon(0.5, 0.5, 0.48), + aes(x, y), fill = "#6B5B95", color = NA) + + geom_polygon(data = create_hexagon(0.5, 0.5, 0.5), + aes(x, y), fill = NA, color = "black", size = 2) + + # Network edges + geom_segment(data = edges, + aes(x = x, y = y, xend = xend, yend = yend), + color = "white", alpha = 0.3, size = 0.5) + + # Network nodes + geom_point(data = nodes, + aes(x, y, size = size), + color = "white", alpha = 0.8) + + # Document icons (simplified as rectangles with text) + geom_rect(data = docs, + aes(xmin = x - 0.03, xmax = x + 0.03, + ymin = y - 0.02, ymax = y + 0.02), + fill = "white", alpha = 0.9) + + geom_text(data = docs, + aes(x, y, label = label, size = size), + color = "#6B5B95", family = "mono") + + # Package name + annotate("text", x = 0.5, y = 0.1, label = "putior", + size = 8, color = "white", fontface = "bold") + + # Styling + scale_size_identity() + + coord_fixed() + + theme_void() + + theme(plot.margin = margin(0, 0, 0, 0)) + +# Approach 3: Grid graphics +# Pros: +# - Low-level control +# - Can create exact shapes and gradients +# - Efficient for complex custom graphics +# Cons: +# - Steeper learning curve +# - More verbose code +# - Less integration with data visualization tools + +create_hex_grid <- function() { + grid.newpage() + + # Define hexagon vertices + angles <- seq(30, 330, by = 60) * pi / 180 + hex_x <- 0.5 + 0.4 * cos(angles) + hex_y <- 0.5 + 0.4 * sin(angles) + + # Create gradient background + gradient <- linearGradient( + colours = c("#4B4B6B", "#6B5B95", "#8B7BA5"), + x1 = 0, y1 = 1, x2 = 0, y2 = 0 + ) + + # Draw hexagon with gradient + grid.polygon(x = hex_x, y = hex_y, + gp = gpar(fill = "#6B5B95", col = "black", lwd = 3)) + + # Add network elements + # ... (would add circles, lines, text using grid functions) + + # Add text + grid.text("putior", x = 0.5, y = 0.1, + gp = gpar(col = "white", fontsize = 24, fontface = "bold")) +} + +# Approach 4: Hybrid approach - ggplot2 + hexSticker +# Pros: +# - Best of both worlds +# - Create complex plot in ggplot2, use hexSticker for final formatting +# - Handles fonts and borders well +# Cons: +# - Requires hexSticker package + +# Approach 5: Using additional packages +# - ggraph: Excellent for network visualizations +# - patchwork: Combine multiple plots +# - ggforce: Additional geoms for complex shapes +# - gganimate: For animated hex stickers + +# Recommendation for putior hex sticker: +# +# Given the requirements: +# 1. Multiple document/file icons +# 2. Network/graph visualization +# 3. Gradient background +# 4. Text elements +# +# Best approach: Pure ggplot2 or ggplot2 + hexSticker +# +# Pure ggplot2 advantages: +# - No additional dependencies +# - Full control over all elements +# - Can create gradient effects with multiple polygons +# - Easy to maintain and modify +# +# Implementation strategy: +# 1. Create hexagon shape as polygon +# 2. Layer gradient effect with multiple semi-transparent polygons +# 3. Use ggraph or manual positioning for network elements +# 4. Add document shapes as rectangles or custom polygons +# 5. Use annotate() or geom_text() for text elements +# 6. Export with ggsave() at appropriate resolution + +# Example implementation starter: +create_putior_hex <- function() { + # This would be the full implementation + # combining all elements shown above + # with proper styling and positioning +} + +# Save examples # ggsave("hex_ggplot.png", hex_ggplot, width = 2, height = 2, dpi = 300) \ No newline at end of file diff --git a/inst/examples/multiline-example.R b/inst/examples/multiline-example.R index 8eb5cf4..953c8d8 100644 --- a/inst/examples/multiline-example.R +++ b/inst/examples/multiline-example.R @@ -1,77 +1,77 @@ -# Multiline PUT Annotation Example -# -# This example demonstrates how to use multiline annotations -# for better code style compliance when dealing with long lists -# of input/output files. - -library(putior) - -# Create temporary directory for example -temp_dir <- tempdir() -example_dir <- file.path(temp_dir, "multiline_example") -dir.create(example_dir, showWarnings = FALSE) - -# Example 1: Traditional single-line annotation (can get very long) -single_line_content <- c( - "# Traditional approach - can be very long and violate style guidelines", - "# put id:\"data_merger\", label:\"Merge Multiple Data Sources\", node_type:\"process\", input:\"sales_data.csv,customer_data.csv,product_data.csv,inventory_data.csv,returns_data.csv\", output:\"merged_dataset.csv\"", - "", - "# Simulate data merging code", - "cat(\"Merging multiple data sources...\\n\")" -) - -# Example 2: Multiline annotation (more readable and style-compliant) -multiline_content <- c( - "# Multiline approach - readable and follows style guidelines", - "# put id:\"data_processor\", \\", - "# label:\"Advanced Data Processing Pipeline\", \\", - "# node_type:\"process\", \\", - "# input:\"raw_sales.csv,customer_profiles.csv,product_catalog.csv,inventory_status.csv,returns_log.csv\", \\", - "# output:\"processed_dataset.csv,summary_report.csv\"", - "", - "# Simulate advanced processing code", - "cat(\"Processing data with complex transformations...\\n\")", - "", - "# Another multiline example", - "# put id:\"visualization_generator\", \\", - "# label:\"Generate Comprehensive Visualizations\", \\", - "# node_type:\"output\", \\", - "# input:\"processed_dataset.csv,summary_report.csv\", \\", - "# output:\"dashboard.html,charts.png,summary_plots.pdf\"", - "", - "cat(\"Generating visualizations...\\n\")" -) - -# Write example files -writeLines(single_line_content, file.path(example_dir, "01_single_line.R")) -writeLines(multiline_content, file.path(example_dir, "02_multiline.R")) - -# Extract workflow from both approaches -cat("\\n=== Extracting PUT annotations ===\\n") -workflow <- put(example_dir) - -# Display results -cat("\\nFound", nrow(workflow), "annotations:\\n") -for (i in 1:nrow(workflow)) { - cat("\\n", i, ". ID:", workflow$id[i], "\\n") - cat(" Label:", workflow$label[i], "\\n") - cat(" Input:", workflow$input[i], "\\n") - cat(" Output:", workflow$output[i], "\\n") -} - -# Generate diagram -cat("\\n=== Generated Mermaid Diagram ===\\n") -diagram <- put_diagram(workflow, theme = "minimal") -cat(diagram) - -# Key benefits of multiline annotations: -cat("\\n\\n=== Benefits of Multiline Annotations ===\\n") -cat("1. Better code readability\\n") -cat("2. Compliance with style guidelines (e.g., styler)\\n") -cat("3. Easier maintenance of long parameter lists\\n") -cat("4. Consistent indentation with surrounding code\\n") -cat("5. No change to existing functionality\\n") - -# Cleanup -unlink(example_dir, recursive = TRUE) +# Multiline PUT Annotation Example +# +# This example demonstrates how to use multiline annotations +# for better code style compliance when dealing with long lists +# of input/output files. + +library(putior) + +# Create temporary directory for example +temp_dir <- tempdir() +example_dir <- file.path(temp_dir, "multiline_example") +dir.create(example_dir, showWarnings = FALSE) + +# Example 1: Traditional single-line annotation (can get very long) +single_line_content <- c( + "# Traditional approach - can be very long and violate style guidelines", + "# put id:\"data_merger\", label:\"Merge Multiple Data Sources\", node_type:\"process\", input:\"sales_data.csv,customer_data.csv,product_data.csv,inventory_data.csv,returns_data.csv\", output:\"merged_dataset.csv\"", + "", + "# Simulate data merging code", + "cat(\"Merging multiple data sources...\\n\")" +) + +# Example 2: Multiline annotation (more readable and style-compliant) +multiline_content <- c( + "# Multiline approach - readable and follows style guidelines", + "# put id:\"data_processor\", \\", + "# label:\"Advanced Data Processing Pipeline\", \\", + "# node_type:\"process\", \\", + "# input:\"raw_sales.csv,customer_profiles.csv,product_catalog.csv,inventory_status.csv,returns_log.csv\", \\", + "# output:\"processed_dataset.csv,summary_report.csv\"", + "", + "# Simulate advanced processing code", + "cat(\"Processing data with complex transformations...\\n\")", + "", + "# Another multiline example", + "# put id:\"visualization_generator\", \\", + "# label:\"Generate Comprehensive Visualizations\", \\", + "# node_type:\"output\", \\", + "# input:\"processed_dataset.csv,summary_report.csv\", \\", + "# output:\"dashboard.html,charts.png,summary_plots.pdf\"", + "", + "cat(\"Generating visualizations...\\n\")" +) + +# Write example files +writeLines(single_line_content, file.path(example_dir, "01_single_line.R")) +writeLines(multiline_content, file.path(example_dir, "02_multiline.R")) + +# Extract workflow from both approaches +cat("\\n=== Extracting PUT annotations ===\\n") +workflow <- put(example_dir) + +# Display results +cat("\\nFound", nrow(workflow), "annotations:\\n") +for (i in 1:nrow(workflow)) { + cat("\\n", i, ". ID:", workflow$id[i], "\\n") + cat(" Label:", workflow$label[i], "\\n") + cat(" Input:", workflow$input[i], "\\n") + cat(" Output:", workflow$output[i], "\\n") +} + +# Generate diagram +cat("\\n=== Generated Mermaid Diagram ===\\n") +diagram <- put_diagram(workflow, theme = "minimal") +cat(diagram) + +# Key benefits of multiline annotations: +cat("\\n\\n=== Benefits of Multiline Annotations ===\\n") +cat("1. Better code readability\\n") +cat("2. Compliance with style guidelines (e.g., styler)\\n") +cat("3. Easier maintenance of long parameter lists\\n") +cat("4. Consistent indentation with surrounding code\\n") +cat("5. No change to existing functionality\\n") + +# Cleanup +unlink(example_dir, recursive = TRUE) cat("\\nExample completed! Temporary files cleaned up.\\n") \ No newline at end of file diff --git a/inst/examples/reprex.R b/inst/examples/reprex.R index 8faba4d..edbc6a0 100644 --- a/inst/examples/reprex.R +++ b/inst/examples/reprex.R @@ -1,201 +1,201 @@ -# Putior Package Example: Workflow Extraction -# ============================================================================== -# This example demonstrates putior's core functionality by creating a sample -# multi-language data workflow and extracting its structure using PUT annotations. -# -# To run this example: -# source(system.file("examples", "reprex.R", package = "putior")) -# ============================================================================== - -library(putior) - -# Create temporary directory for example files -temp_dir <- file.path(tempdir(), "putior_workflow_example") -dir.create(temp_dir, showWarnings = FALSE) - -cat("Creating example workflow in:", temp_dir, "\n") - -# ============================================================================== -# CREATE SAMPLE WORKFLOW FILES -# ============================================================================== - -# File 1: Data Collection (Python script) -# Demonstrates: input node with output annotation -file1_lines <- c( - "# Data Collection Script", - "# put id:\"collect_data\", label:\"Collect Raw Data\", node_type:\"input\", output:\"raw_data.csv\"", - "", - "import pandas as pd", - "import requests", - "", - "def collect_sales_data():", - " \"\"\"Collect sales data from API\"\"\"", - " # Simulate API call", - " data = pd.DataFrame({", - " \"date\": [\"2024-01-01\", \"2024-01-02\", \"2024-01-03\"],", - " \"sales\": [100, 150, 120],", - " \"region\": [\"North\", \"South\", \"East\"]", - " })", - " data.to_csv(\"raw_data.csv\", index=False)", - " return data", - "", - "if __name__ == \"__main__\":", - " collect_sales_data()" -) - -# File 2: Data Processing (R script) -# Demonstrates: process node with input and output annotations -file2_lines <- c( - "# Data Processing Script", - "# put id:\"process_data\", label:\"Clean and Transform Data\", node_type:\"process\", input:\"raw_data.csv\", output:\"processed_data.csv\"", - "", - "library(dplyr)", - "library(readr)", - "", - "# Read raw data", - "raw_data <- read_csv(\"raw_data.csv\")", - "", - "# Clean and process data", - "processed_data <- raw_data %>%", - " mutate(", - " date = as.Date(date),", - " sales_category = case_when(", - " sales < 110 ~ \"Low\",", - " sales < 140 ~ \"Medium\",", - " TRUE ~ \"High\"", - " )", - " ) %>%", - " arrange(date)", - "", - "# Save processed data", - "write_csv(processed_data, \"processed_data.csv\")" -) - -# File 3: Analysis and Reporting (R script) -# Demonstrates: multiple PUT annotations in one file -file3_lines <- c( - "# Analysis and Reporting Script", - "# put id:\"create_report\", label:\"Generate Sales Report\", node_type:\"output\", input:\"processed_data.csv\", output:\"sales_report.html\"", - "# put id:\"create_summary\", label:\"Calculate Summary Stats\", node_type:\"process\", input:\"processed_data.csv\", output:\"summary_stats.json\"", - "", - "library(dplyr)", - "library(readr)", - "library(jsonlite)", - "", - "# Read processed data", - "data <- read_csv(\"processed_data.csv\")", - "", - "# Create summary statistics", - "summary_stats <- list(", - " total_sales = sum(data$sales),", - " avg_sales = mean(data$sales),", - " sales_by_category = data %>%", - " count(sales_category) %>%", - " setNames(c(\"category\", \"count\"))", - ")", - "", - "# Save summary as JSON", - "write_json(summary_stats, \"summary_stats.json\", pretty = TRUE)", - "", - "# Generate HTML report (simplified)", - "report_html <- paste0(", - " \"
\",", - " \"Total Sales: \", summary_stats$total_sales, \"
\",", - " \"Average Sales: \", round(summary_stats$avg_sales, 2), \"
\",", - " \"\"", - ")", - "", - "writeLines(report_html, \"sales_report.html\")" -) - -# Write example files -writeLines(file1_lines, file.path(temp_dir, "01_collect_data.py")) -writeLines(file2_lines, file.path(temp_dir, "02_process_data.R")) -writeLines(file3_lines, file.path(temp_dir, "03_analyze_report.R")) - -cat("Created", length(list.files(temp_dir)), "example files\n") - -# ============================================================================== -# EXTRACT WORKFLOW USING PUTIOR -# ============================================================================== - -cat("\n=== Extracting workflow with putior::put() ===\n") -workflow <- put(temp_dir) - -# Display results -cat("\nWorkflow extraction results:\n") -print(workflow) - -# ============================================================================== -# ANALYZE WORKFLOW STRUCTURE -# ============================================================================== - -if (nrow(workflow) > 0) { - - cat("\n=== Workflow Analysis ===\n") - - # Data lineage summary - cat("\nData Flow Summary:\n") - inputs <- unique(workflow$input[!is.na(workflow$input)]) - outputs <- unique(workflow$output[!is.na(workflow$output)]) - - cat(" External Inputs:", setdiff(inputs, outputs), "\n") - cat(" Intermediate Files:", intersect(inputs, outputs), "\n") - cat(" Final Outputs:", setdiff(outputs, inputs), "\n") - - # Node types - cat("\nNode Types:\n") - node_types <- table(workflow$node_type) - for (i in seq_along(node_types)) { - cat(" ", names(node_types)[i], ":", node_types[i], "nodes\n") - } - - # File types - cat("\nFile Types:\n") - file_types <- table(workflow$file_type) - for (i in seq_along(file_types)) { - cat(" ", toupper(names(file_types)[i]), ":", file_types[i], "files\n") - } - - # Detailed workflow steps - cat("\nDetailed Workflow Steps:\n") - for (i in 1:nrow(workflow)) { - row <- workflow[i, ] - cat(" Step", i, ":", row$id, "\n") - cat(" File:", row$file_name, "(", row$file_type, ")\n") - cat(" Action:", row$label, "\n") - cat(" Type:", row$node_type, "\n") - if (!is.na(row$input)) cat(" Consumes:", row$input, "\n") - if (!is.na(row$output)) cat(" Produces:", row$output, "\n") - cat("\n") - } - -} else { - cat("No workflow annotations found. Check PUT annotation syntax.\n") -} - -# ============================================================================== -# CLEANUP AND NEXT STEPS -# ============================================================================== - -cat("=== Example Complete ===\n") -cat("Example files location:", temp_dir, "\n") -cat("\nTo explore further:\n") -cat("1. Examine the generated files to see PUT annotation syntax\n") -cat("2. Try modifying annotations and re-running put()\n") -cat("3. Add your own workflow files with PUT annotations\n") - -# Optional: Clean up temporary files -cat("\nClean up temporary files? (y/n): ") -if (interactive()) { - response <- readline() - if (tolower(response) == "y") { - unlink(temp_dir, recursive = TRUE) - cat("Temporary files removed.\n") - } else { - cat("Temporary files preserved for inspection.\n") - } -} else { - cat("(Running non-interactively - files preserved)\n") +# Putior Package Example: Workflow Extraction +# ============================================================================== +# This example demonstrates putior's core functionality by creating a sample +# multi-language data workflow and extracting its structure using PUT annotations. +# +# To run this example: +# source(system.file("examples", "reprex.R", package = "putior")) +# ============================================================================== + +library(putior) + +# Create temporary directory for example files +temp_dir <- file.path(tempdir(), "putior_workflow_example") +dir.create(temp_dir, showWarnings = FALSE) + +cat("Creating example workflow in:", temp_dir, "\n") + +# ============================================================================== +# CREATE SAMPLE WORKFLOW FILES +# ============================================================================== + +# File 1: Data Collection (Python script) +# Demonstrates: input node with output annotation +file1_lines <- c( + "# Data Collection Script", + "# put id:\"collect_data\", label:\"Collect Raw Data\", node_type:\"input\", output:\"raw_data.csv\"", + "", + "import pandas as pd", + "import requests", + "", + "def collect_sales_data():", + " \"\"\"Collect sales data from API\"\"\"", + " # Simulate API call", + " data = pd.DataFrame({", + " \"date\": [\"2024-01-01\", \"2024-01-02\", \"2024-01-03\"],", + " \"sales\": [100, 150, 120],", + " \"region\": [\"North\", \"South\", \"East\"]", + " })", + " data.to_csv(\"raw_data.csv\", index=False)", + " return data", + "", + "if __name__ == \"__main__\":", + " collect_sales_data()" +) + +# File 2: Data Processing (R script) +# Demonstrates: process node with input and output annotations +file2_lines <- c( + "# Data Processing Script", + "# put id:\"process_data\", label:\"Clean and Transform Data\", node_type:\"process\", input:\"raw_data.csv\", output:\"processed_data.csv\"", + "", + "library(dplyr)", + "library(readr)", + "", + "# Read raw data", + "raw_data <- read_csv(\"raw_data.csv\")", + "", + "# Clean and process data", + "processed_data <- raw_data %>%", + " mutate(", + " date = as.Date(date),", + " sales_category = case_when(", + " sales < 110 ~ \"Low\",", + " sales < 140 ~ \"Medium\",", + " TRUE ~ \"High\"", + " )", + " ) %>%", + " arrange(date)", + "", + "# Save processed data", + "write_csv(processed_data, \"processed_data.csv\")" +) + +# File 3: Analysis and Reporting (R script) +# Demonstrates: multiple PUT annotations in one file +file3_lines <- c( + "# Analysis and Reporting Script", + "# put id:\"create_report\", label:\"Generate Sales Report\", node_type:\"output\", input:\"processed_data.csv\", output:\"sales_report.html\"", + "# put id:\"create_summary\", label:\"Calculate Summary Stats\", node_type:\"process\", input:\"processed_data.csv\", output:\"summary_stats.json\"", + "", + "library(dplyr)", + "library(readr)", + "library(jsonlite)", + "", + "# Read processed data", + "data <- read_csv(\"processed_data.csv\")", + "", + "# Create summary statistics", + "summary_stats <- list(", + " total_sales = sum(data$sales),", + " avg_sales = mean(data$sales),", + " sales_by_category = data %>%", + " count(sales_category) %>%", + " setNames(c(\"category\", \"count\"))", + ")", + "", + "# Save summary as JSON", + "write_json(summary_stats, \"summary_stats.json\", pretty = TRUE)", + "", + "# Generate HTML report (simplified)", + "report_html <- paste0(", + " \"\",", + " \"Total Sales: \", summary_stats$total_sales, \"
\",", + " \"Average Sales: \", round(summary_stats$avg_sales, 2), \"
\",", + " \"\"", + ")", + "", + "writeLines(report_html, \"sales_report.html\")" +) + +# Write example files +writeLines(file1_lines, file.path(temp_dir, "01_collect_data.py")) +writeLines(file2_lines, file.path(temp_dir, "02_process_data.R")) +writeLines(file3_lines, file.path(temp_dir, "03_analyze_report.R")) + +cat("Created", length(list.files(temp_dir)), "example files\n") + +# ============================================================================== +# EXTRACT WORKFLOW USING PUTIOR +# ============================================================================== + +cat("\n=== Extracting workflow with putior::put() ===\n") +workflow <- put(temp_dir) + +# Display results +cat("\nWorkflow extraction results:\n") +print(workflow) + +# ============================================================================== +# ANALYZE WORKFLOW STRUCTURE +# ============================================================================== + +if (nrow(workflow) > 0) { + + cat("\n=== Workflow Analysis ===\n") + + # Data lineage summary + cat("\nData Flow Summary:\n") + inputs <- unique(workflow$input[!is.na(workflow$input)]) + outputs <- unique(workflow$output[!is.na(workflow$output)]) + + cat(" External Inputs:", setdiff(inputs, outputs), "\n") + cat(" Intermediate Files:", intersect(inputs, outputs), "\n") + cat(" Final Outputs:", setdiff(outputs, inputs), "\n") + + # Node types + cat("\nNode Types:\n") + node_types <- table(workflow$node_type) + for (i in seq_along(node_types)) { + cat(" ", names(node_types)[i], ":", node_types[i], "nodes\n") + } + + # File types + cat("\nFile Types:\n") + file_types <- table(workflow$file_type) + for (i in seq_along(file_types)) { + cat(" ", toupper(names(file_types)[i]), ":", file_types[i], "files\n") + } + + # Detailed workflow steps + cat("\nDetailed Workflow Steps:\n") + for (i in 1:nrow(workflow)) { + row <- workflow[i, ] + cat(" Step", i, ":", row$id, "\n") + cat(" File:", row$file_name, "(", row$file_type, ")\n") + cat(" Action:", row$label, "\n") + cat(" Type:", row$node_type, "\n") + if (!is.na(row$input)) cat(" Consumes:", row$input, "\n") + if (!is.na(row$output)) cat(" Produces:", row$output, "\n") + cat("\n") + } + +} else { + cat("No workflow annotations found. Check PUT annotation syntax.\n") +} + +# ============================================================================== +# CLEANUP AND NEXT STEPS +# ============================================================================== + +cat("=== Example Complete ===\n") +cat("Example files location:", temp_dir, "\n") +cat("\nTo explore further:\n") +cat("1. Examine the generated files to see PUT annotation syntax\n") +cat("2. Try modifying annotations and re-running put()\n") +cat("3. Add your own workflow files with PUT annotations\n") + +# Optional: Clean up temporary files +cat("\nClean up temporary files? (y/n): ") +if (interactive()) { + response <- readline() + if (tolower(response) == "y") { + unlink(temp_dir, recursive = TRUE) + cat("Temporary files removed.\n") + } else { + cat("Temporary files preserved for inspection.\n") + } +} else { + cat("(Running non-interactively - files preserved)\n") } \ No newline at end of file diff --git a/inst/examples/self-documentation.R b/inst/examples/self-documentation.R index ff7c607..3aa0a69 100644 --- a/inst/examples/self-documentation.R +++ b/inst/examples/self-documentation.R @@ -1,46 +1,46 @@ -#!/usr/bin/env Rscript -# Example: How putior documents its own workflow -# This demonstrates how to add PUT annotations to your own R package - -library(putior) - -# Scan the putior package itself for PUT annotations -cat("Scanning putior package for workflow annotations...\n\n") -workflow <- put('./R/') - -# Show the extracted workflow data -cat("Extracted workflow data:\n") -print(workflow) - -cat("\n\n=== Simple Script-to-Script View ===\n") -# Generate a simple diagram showing only script connections -put_diagram(workflow, - title = "Putior Package Workflow", - show_workflow_boundaries = TRUE) - -cat("\n\n=== Complete Data Flow View ===\n") -# Generate a complete diagram showing all data artifacts -put_diagram(workflow, - title = "Putior Data Flow with Artifacts", - show_artifacts = TRUE, - show_workflow_boundaries = TRUE) - -cat("\n\n=== GitHub-optimized Diagram ===\n") -# Generate a GitHub-friendly diagram -put_diagram(workflow, - title = "Putior Workflow (GitHub Theme)", - theme = "github", - show_workflow_boundaries = TRUE) - -# Explanation of the workflow -cat("\n\nWorkflow Explanation:\n") -cat("1. put() function (start) - Entry point that scans files for annotations\n") -cat("2. process_file() - Processes each file to extract annotations\n") -cat("3. parse_put_annotation() - Parses the PUT syntax into properties\n") -cat("4. validate_annotation() - Validates annotation syntax and values\n") -cat("5. convert_results_to_df() - Converts results to a data frame\n") -cat("6. put_diagram() - Generates Mermaid diagram from workflow data\n") -cat("7. generate_node_definitions() - Creates node definitions\n") -cat("8. generate_connections() - Creates edges between nodes\n") -cat("9. generate_node_styling() - Applies theme styling\n") +#!/usr/bin/env Rscript +# Example: How putior documents its own workflow +# This demonstrates how to add PUT annotations to your own R package + +library(putior) + +# Scan the putior package itself for PUT annotations +cat("Scanning putior package for workflow annotations...\n\n") +workflow <- put('./R/') + +# Show the extracted workflow data +cat("Extracted workflow data:\n") +print(workflow) + +cat("\n\n=== Simple Script-to-Script View ===\n") +# Generate a simple diagram showing only script connections +put_diagram(workflow, + title = "Putior Package Workflow", + show_workflow_boundaries = TRUE) + +cat("\n\n=== Complete Data Flow View ===\n") +# Generate a complete diagram showing all data artifacts +put_diagram(workflow, + title = "Putior Data Flow with Artifacts", + show_artifacts = TRUE, + show_workflow_boundaries = TRUE) + +cat("\n\n=== GitHub-optimized Diagram ===\n") +# Generate a GitHub-friendly diagram +put_diagram(workflow, + title = "Putior Workflow (GitHub Theme)", + theme = "github", + show_workflow_boundaries = TRUE) + +# Explanation of the workflow +cat("\n\nWorkflow Explanation:\n") +cat("1. put() function (start) - Entry point that scans files for annotations\n") +cat("2. process_file() - Processes each file to extract annotations\n") +cat("3. parse_put_annotation() - Parses the PUT syntax into properties\n") +cat("4. validate_annotation() - Validates annotation syntax and values\n") +cat("5. convert_results_to_df() - Converts results to a data frame\n") +cat("6. put_diagram() - Generates Mermaid diagram from workflow data\n") +cat("7. generate_node_definitions() - Creates node definitions\n") +cat("8. generate_connections() - Creates edges between nodes\n") +cat("9. generate_node_styling() - Applies theme styling\n") cat("10. handle_output() (end) - Outputs the final diagram\n") \ No newline at end of file diff --git a/inst/examples/source-tracking-example.R b/inst/examples/source-tracking-example.R index fc5190d..bc42a80 100644 --- a/inst/examples/source-tracking-example.R +++ b/inst/examples/source-tracking-example.R @@ -1,133 +1,133 @@ -# Source Tracking Example: Demonstrating how to annotate source() relationships -# ============================================================================== -# This example shows how to properly annotate scripts that source other scripts -# to track the source() relationships in putior workflows. -# -# To run this example: -# source(system.file("examples", "source-tracking-example.R", package = "putior")) -# ============================================================================== - -library(putior) - -cat("๐ Source Tracking Example: Proper source() Annotation\n") -cat(paste(rep("=", 55), collapse = ""), "\n\n") - -# Create example directory structure -temp_dir <- file.path(tempdir(), "putior_source_example") -dir.create(temp_dir, showWarnings = FALSE) - -cat("๐ Creating example workflow with source() relationships...\n") - -# Main orchestrator script -main_script <- c( - "# Main workflow script that sources utility modules", - "# put label:\"Main Analysis Pipeline\", input:\"utils.R,analysis.R,plotting.R\", output:\"final_results.csv\"", - "", - "# Source utility modules (reading them INTO this script)", - "source(\"utils.R\") # Reading utils.R into main.R", - "source(\"analysis.R\") # Reading analysis.R into main.R", - "source(\"plotting.R\") # Reading plotting.R into main.R", - "", - "# Execute the pipeline", - "data <- load_and_clean(\"raw_data.csv\")", - "results <- perform_analysis(data)", - "create_plots(results)", - "write.csv(results, \"final_results.csv\")" -) - -# Utility functions script -utils_script <- c( - "# Utility functions - sourced BY main.R", - "# put label:\"Data Utilities\", node_type:\"input\"", - "# output defaults to 'utils.R' - this script provides itself", - "", - "load_and_clean <- function(file) {", - " # Load and clean data", - " data <- read.csv(file)", - " data[complete.cases(data), ]", - "}" -) - -# Analysis functions script -analysis_script <- c( - "# Analysis functions - sourced BY main.R, depends on utils.R", - "# put label:\"Statistical Analysis\", node_type:\"process\", input:\"utils.R\"", - "# input: utils.R because we depend on functions from utils.R", - "# output defaults to 'analysis.R'", - "", - "perform_analysis <- function(data) {", - " # Perform statistical analysis", - " # Uses load_and_clean() from utils.R", - " summary(data)", - "}" -) - -# Plotting functions script -plotting_script <- c( - "# Plotting functions - sourced BY main.R, depends on analysis.R", - "# put label:\"Data Visualization\", node_type:\"output\", input:\"analysis.R\"", - "# input: analysis.R because we depend on analysis functions", - "# output defaults to 'plotting.R'", - "", - "create_plots <- function(results) {", - " # Create visualizations", - " # Uses perform_analysis() results", - " plot(results)", - "}" -) - -# Write all files -writeLines(main_script, file.path(temp_dir, "main.R")) -writeLines(utils_script, file.path(temp_dir, "utils.R")) -writeLines(analysis_script, file.path(temp_dir, "analysis.R")) -writeLines(plotting_script, file.path(temp_dir, "plotting.R")) - -cat("โ Created source tracking example files\n\n") - -# Extract workflow -cat("๐ Extracting source() relationships...\n") -workflow <- put(temp_dir) - -cat("\n๐ Source tracking results:\n") -cat(paste(rep("-", 50), collapse = ""), "\n") - -# Display the relationships -for (i in seq_len(nrow(workflow))) { - row <- workflow[i, ] - cat(sprintf("%-15s: %s\n", "File", row$file_name)) - cat(sprintf("%-15s: %s\n", "Label", row$label)) - cat(sprintf("%-15s: %s\n", "Node Type", ifelse(is.na(row$node_type), "process", row$node_type))) - cat(sprintf("%-15s: %s\n", "Input", ifelse(is.na(row$input), "none", row$input))) - cat(sprintf("%-15s: %s\n", "Output", row$output)) - cat(paste(rep("-", 50), collapse = ""), "\n") -} - -# Generate diagram -cat("\n๐จ Source relationship diagram:\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - theme = "github", - show_files = TRUE, - title = "Source Tracking Example") - -cat("\n\n๐ก Key Insights:\n") -cat(paste(rep("=", 50), collapse = ""), "\n") -cat("1. Scripts being sourced are INPUTS to the main script\n") -cat("2. Flow direction: sourced scripts โ main script\n") -cat("3. main.R reads utils.R, analysis.R, and plotting.R\n") -cat("4. Dependencies: utils.R โ analysis.R โ plotting.R\n") -cat("5. Arrows with file names show the source() relationships\n\n") - -cat("๐ Annotation Pattern Summary:\n") -cat("โข Main script: input=\"script1.R,script2.R,script3.R\"\n") -cat("โข Sourced scripts: output defaults to their filename\n") -cat("โข Dependencies: input=\"dependency_script.R\"\n\n") - -cat("๐๏ธ Example files created in:\n") -cat(" ", temp_dir, "\n\n") - -cat("โ Source tracking example complete!\n") - -# Clean up -cat("๐งน Cleaning up...\n") +# Source Tracking Example: Demonstrating how to annotate source() relationships +# ============================================================================== +# This example shows how to properly annotate scripts that source other scripts +# to track the source() relationships in putior workflows. +# +# To run this example: +# source(system.file("examples", "source-tracking-example.R", package = "putior")) +# ============================================================================== + +library(putior) + +cat("๐ Source Tracking Example: Proper source() Annotation\n") +cat(paste(rep("=", 55), collapse = ""), "\n\n") + +# Create example directory structure +temp_dir <- file.path(tempdir(), "putior_source_example") +dir.create(temp_dir, showWarnings = FALSE) + +cat("๐ Creating example workflow with source() relationships...\n") + +# Main orchestrator script +main_script <- c( + "# Main workflow script that sources utility modules", + "# put label:\"Main Analysis Pipeline\", input:\"utils.R,analysis.R,plotting.R\", output:\"final_results.csv\"", + "", + "# Source utility modules (reading them INTO this script)", + "source(\"utils.R\") # Reading utils.R into main.R", + "source(\"analysis.R\") # Reading analysis.R into main.R", + "source(\"plotting.R\") # Reading plotting.R into main.R", + "", + "# Execute the pipeline", + "data <- load_and_clean(\"raw_data.csv\")", + "results <- perform_analysis(data)", + "create_plots(results)", + "write.csv(results, \"final_results.csv\")" +) + +# Utility functions script +utils_script <- c( + "# Utility functions - sourced BY main.R", + "# put label:\"Data Utilities\", node_type:\"input\"", + "# output defaults to 'utils.R' - this script provides itself", + "", + "load_and_clean <- function(file) {", + " # Load and clean data", + " data <- read.csv(file)", + " data[complete.cases(data), ]", + "}" +) + +# Analysis functions script +analysis_script <- c( + "# Analysis functions - sourced BY main.R, depends on utils.R", + "# put label:\"Statistical Analysis\", node_type:\"process\", input:\"utils.R\"", + "# input: utils.R because we depend on functions from utils.R", + "# output defaults to 'analysis.R'", + "", + "perform_analysis <- function(data) {", + " # Perform statistical analysis", + " # Uses load_and_clean() from utils.R", + " summary(data)", + "}" +) + +# Plotting functions script +plotting_script <- c( + "# Plotting functions - sourced BY main.R, depends on analysis.R", + "# put label:\"Data Visualization\", node_type:\"output\", input:\"analysis.R\"", + "# input: analysis.R because we depend on analysis functions", + "# output defaults to 'plotting.R'", + "", + "create_plots <- function(results) {", + " # Create visualizations", + " # Uses perform_analysis() results", + " plot(results)", + "}" +) + +# Write all files +writeLines(main_script, file.path(temp_dir, "main.R")) +writeLines(utils_script, file.path(temp_dir, "utils.R")) +writeLines(analysis_script, file.path(temp_dir, "analysis.R")) +writeLines(plotting_script, file.path(temp_dir, "plotting.R")) + +cat("โ Created source tracking example files\n\n") + +# Extract workflow +cat("๐ Extracting source() relationships...\n") +workflow <- put(temp_dir) + +cat("\n๐ Source tracking results:\n") +cat(paste(rep("-", 50), collapse = ""), "\n") + +# Display the relationships +for (i in seq_len(nrow(workflow))) { + row <- workflow[i, ] + cat(sprintf("%-15s: %s\n", "File", row$file_name)) + cat(sprintf("%-15s: %s\n", "Label", row$label)) + cat(sprintf("%-15s: %s\n", "Node Type", ifelse(is.na(row$node_type), "process", row$node_type))) + cat(sprintf("%-15s: %s\n", "Input", ifelse(is.na(row$input), "none", row$input))) + cat(sprintf("%-15s: %s\n", "Output", row$output)) + cat(paste(rep("-", 50), collapse = ""), "\n") +} + +# Generate diagram +cat("\n๐จ Source relationship diagram:\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + theme = "github", + show_files = TRUE, + title = "Source Tracking Example") + +cat("\n\n๐ก Key Insights:\n") +cat(paste(rep("=", 50), collapse = ""), "\n") +cat("1. Scripts being sourced are INPUTS to the main script\n") +cat("2. Flow direction: sourced scripts โ main script\n") +cat("3. main.R reads utils.R, analysis.R, and plotting.R\n") +cat("4. Dependencies: utils.R โ analysis.R โ plotting.R\n") +cat("5. Arrows with file names show the source() relationships\n\n") + +cat("๐ Annotation Pattern Summary:\n") +cat("โข Main script: input=\"script1.R,script2.R,script3.R\"\n") +cat("โข Sourced scripts: output defaults to their filename\n") +cat("โข Dependencies: input=\"dependency_script.R\"\n\n") + +cat("๐๏ธ Example files created in:\n") +cat(" ", temp_dir, "\n\n") + +cat("โ Source tracking example complete!\n") + +# Clean up +cat("๐งน Cleaning up...\n") unlink(temp_dir, recursive = TRUE) \ No newline at end of file diff --git a/inst/examples/theme-examples.R b/inst/examples/theme-examples.R index bdf0764..934f5ab 100644 --- a/inst/examples/theme-examples.R +++ b/inst/examples/theme-examples.R @@ -1,220 +1,220 @@ -# Putior Theme Examples: Standard and Colorblind-Safe Themes -# ============================================================================== -# This example demonstrates putior's 9 themes including colorblind-safe options. -# -# Standard themes: light, dark, auto, minimal, github -# Colorblind-safe (viridis family): viridis, magma, plasma, cividis -# -# To run this example: -# source(system.file("examples", "theme-examples.R", package = "putior")) -# ============================================================================== - -library(putior) - -cat("๐จ Putior Theme Examples\n") -cat(paste(rep("=", 40), collapse = ""), "\n\n") - -# Create a sample workflow for theme demonstration -temp_dir <- file.path(tempdir(), "putior_themes") -dir.create(temp_dir, showWarnings = FALSE) - -cat("๐ Creating sample workflow...\n") - -# Simple but representative workflow -workflow_files <- list( - "collect.py" = c( - "# put id:\"fetch_data\", label:\"Fetch API Data\", node_type:\"input\", output:\"raw_data.json\"", - "import requests", - "data = requests.get('/api/data').json()", - "with open('raw_data.json', 'w') as f:", - " json.dump(data, f)" - ), - - "process.R" = c( - "# put id:\"clean_data\", label:\"Clean and Validate\", node_type:\"process\", input:\"raw_data.json\", output:\"clean_data.csv\"", - "library(jsonlite)", - "raw <- fromJSON('raw_data.json')", - "clean <- clean_dataset(raw)", - "write.csv(clean, 'clean_data.csv')" - ), - - "analyze.R" = c( - "# put id:\"statistical_analysis\", label:\"Statistical Analysis\", node_type:\"process\", input:\"clean_data.csv\", output:\"results.rds\"", - "# put id:\"quality_check\", label:\"Data Quality Check\", node_type:\"decision\", input:\"clean_data.csv\", output:\"quality_report.json\"", - "data <- read.csv('clean_data.csv')", - "results <- perform_analysis(data)", - "saveRDS(results, 'results.rds')" - ), - - "report.R" = c( - "# put id:\"generate_report\", label:\"Generate Final Report\", node_type:\"output\", input:\"results.rds\", output:\"final_report.html\"", - "results <- readRDS('results.rds')", - "rmarkdown::render('report_template.Rmd', output_file = 'final_report.html')" - ) -) - -# Write workflow files -for (filename in names(workflow_files)) { - writeLines(workflow_files[[filename]], file.path(temp_dir, filename)) -} - -# Extract workflow -workflow <- put(temp_dir) -cat("โ Found", nrow(workflow), "workflow nodes\n\n") - -# Show available themes -cat("๐จ Available Themes:\n") -themes <- get_diagram_themes() -for (theme_name in names(themes)) { - cat(" ", theme_name, ":", themes[[theme_name]], "\n") -} -cat("\n") - -cat(paste(rep("=", 50), collapse = ""), "\n") -cat("๐ LIGHT THEME (Default)\n") -cat("Perfect for: Documentation sites, light mode environments\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Light Theme", - theme = "light") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ DARK THEME\n") -cat("Perfect for: Dark mode environments, terminal displays\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Dark Theme", - theme = "dark", - direction = "LR") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ AUTO THEME (GitHub Adaptive)\n") -cat("Perfect for: GitHub README files, automatically adapts to user's theme\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Auto Theme", - theme = "auto", - show_files = TRUE) - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("โช MINIMAL THEME\n") -cat("Perfect for: Professional documents, print-friendly diagrams\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Minimal Theme", - theme = "minimal", - node_labels = "both") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ฏ VIRIDIS THEME (Colorblind-Safe)\n") -cat("Perfect for: Accessibility, general use, perceptually uniform\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Viridis Theme", - theme = "viridis") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ฅ MAGMA THEME (Colorblind-Safe, Warm)\n") -cat("Perfect for: High contrast, print materials\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Magma Theme", - theme = "magma") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ PLASMA THEME (Colorblind-Safe, Vibrant)\n") -cat("Perfect for: Presentations, digital displays\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Plasma Theme", - theme = "plasma", - direction = "LR") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐๏ธ CIVIDIS THEME (Maximum Colorblind Accessibility)\n") -cat("Perfect for: Red-green colorblindness (deuteranopia/protanopia)\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow, - title = "Data Processing Pipeline - Cividis Theme", - theme = "cividis") - -cat("\n", paste(rep("=", 50), collapse = ""), "\n") -cat("๐ก USAGE RECOMMENDATIONS\n") -cat(paste(rep("=", 50), collapse = ""), "\n\n") - -cat("๐ Light Theme:\n") -cat(" โข Default theme with bright, friendly colors\n") -cat(" โข Best for documentation websites and light backgrounds\n") -cat(" โข High contrast and readability\n\n") - -cat("๐ Dark Theme:\n") -cat(" โข Muted colors with light text\n") -cat(" โข Perfect for dark mode applications and terminals\n") -cat(" โข Easy on the eyes for extended viewing\n\n") - -cat("๐ Auto Theme:\n") -cat(" โข Uses GitHub's adaptive color system\n") -cat(" โข Automatically looks good in both light and dark modes\n") -cat(" โข Recommended for GitHub README files\n\n") - -cat("โช Minimal Theme:\n") -cat(" โข Grayscale with subtle borders\n") -cat(" โข Professional appearance for business documents\n") -cat(" โข Print-friendly and accessible\n\n") - -cat("๐ฏ Viridis Theme (Colorblind-Safe):\n") -cat(" โข Purple โ Blue โ Green โ Yellow palette\n") -cat(" โข Most widely tested colorblind-safe palette\n") -cat(" โข Perceptually uniform - equal steps appear equally different\n\n") - -cat("๐ฅ Magma Theme (Colorblind-Safe):\n") -cat(" โข Purple โ Red โ Yellow warm palette\n") -cat(" โข High contrast for print and presentations\n") -cat(" โข Works well in grayscale\n\n") - -cat("๐ Plasma Theme (Colorblind-Safe):\n") -cat(" โข Purple โ Pink โ Orange โ Yellow vibrant palette\n") -cat(" โข Bold colors ideal for digital displays\n") -cat(" โข Great for presentations\n\n") - -cat("๐๏ธ Cividis Theme (Maximum Accessibility):\n") -cat(" โข Blue โ Gray โ Yellow palette\n") -cat(" โข Specifically optimized for deuteranopia and protanopia\n") -cat(" โข Avoids red-green entirely\n\n") - -cat("๐ Example Usage:\n") -cat(" # For GitHub README (adapts to user's theme)\n") -cat(" put_diagram(workflow, theme = 'auto')\n\n") -cat(" # For dark documentation sites\n") -cat(" put_diagram(workflow, theme = 'dark', direction = 'LR')\n\n") -cat(" # For professional reports\n") -cat(" put_diagram(workflow, theme = 'minimal', output = 'file')\n\n") -cat(" # For colorblind-safe diagrams\n") -cat(" put_diagram(workflow, theme = 'viridis') # General accessibility\n") -cat(" put_diagram(workflow, theme = 'cividis') # Red-green colorblindness\n\n") - -# Save examples to files for comparison -cat("๐พ Saving theme examples to files...\n") - -themes_to_save <- c("light", "dark", "auto", "minimal", "github", - "viridis", "magma", "plasma", "cividis") -for (theme in themes_to_save) { - filename <- file.path(temp_dir, paste0("workflow_", theme, "_theme.md")) - put_diagram(workflow, - output = "file", - file = filename, - title = paste("Workflow -", stringr::str_to_title(theme), "Theme"), - theme = theme, - show_files = TRUE) -} - -cat("โ Saved theme examples:\n") -for (theme in themes_to_save) { - filename <- paste0("workflow_", theme, "_theme.md") - cat(" โข", filename, "\n") -} - -cat("\n๐ All files saved to:", temp_dir, "\n") -cat("๐ก Try opening these files in different environments to see the themes!\n\n") - +# Putior Theme Examples: Standard and Colorblind-Safe Themes +# ============================================================================== +# This example demonstrates putior's 9 themes including colorblind-safe options. +# +# Standard themes: light, dark, auto, minimal, github +# Colorblind-safe (viridis family): viridis, magma, plasma, cividis +# +# To run this example: +# source(system.file("examples", "theme-examples.R", package = "putior")) +# ============================================================================== + +library(putior) + +cat("๐จ Putior Theme Examples\n") +cat(paste(rep("=", 40), collapse = ""), "\n\n") + +# Create a sample workflow for theme demonstration +temp_dir <- file.path(tempdir(), "putior_themes") +dir.create(temp_dir, showWarnings = FALSE) + +cat("๐ Creating sample workflow...\n") + +# Simple but representative workflow +workflow_files <- list( + "collect.py" = c( + "# put id:\"fetch_data\", label:\"Fetch API Data\", node_type:\"input\", output:\"raw_data.json\"", + "import requests", + "data = requests.get('/api/data').json()", + "with open('raw_data.json', 'w') as f:", + " json.dump(data, f)" + ), + + "process.R" = c( + "# put id:\"clean_data\", label:\"Clean and Validate\", node_type:\"process\", input:\"raw_data.json\", output:\"clean_data.csv\"", + "library(jsonlite)", + "raw <- fromJSON('raw_data.json')", + "clean <- clean_dataset(raw)", + "write.csv(clean, 'clean_data.csv')" + ), + + "analyze.R" = c( + "# put id:\"statistical_analysis\", label:\"Statistical Analysis\", node_type:\"process\", input:\"clean_data.csv\", output:\"results.rds\"", + "# put id:\"quality_check\", label:\"Data Quality Check\", node_type:\"decision\", input:\"clean_data.csv\", output:\"quality_report.json\"", + "data <- read.csv('clean_data.csv')", + "results <- perform_analysis(data)", + "saveRDS(results, 'results.rds')" + ), + + "report.R" = c( + "# put id:\"generate_report\", label:\"Generate Final Report\", node_type:\"output\", input:\"results.rds\", output:\"final_report.html\"", + "results <- readRDS('results.rds')", + "rmarkdown::render('report_template.Rmd', output_file = 'final_report.html')" + ) +) + +# Write workflow files +for (filename in names(workflow_files)) { + writeLines(workflow_files[[filename]], file.path(temp_dir, filename)) +} + +# Extract workflow +workflow <- put(temp_dir) +cat("โ Found", nrow(workflow), "workflow nodes\n\n") + +# Show available themes +cat("๐จ Available Themes:\n") +themes <- get_diagram_themes() +for (theme_name in names(themes)) { + cat(" ", theme_name, ":", themes[[theme_name]], "\n") +} +cat("\n") + +cat(paste(rep("=", 50), collapse = ""), "\n") +cat("๐ LIGHT THEME (Default)\n") +cat("Perfect for: Documentation sites, light mode environments\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Light Theme", + theme = "light") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ DARK THEME\n") +cat("Perfect for: Dark mode environments, terminal displays\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Dark Theme", + theme = "dark", + direction = "LR") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ AUTO THEME (GitHub Adaptive)\n") +cat("Perfect for: GitHub README files, automatically adapts to user's theme\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Auto Theme", + theme = "auto", + show_files = TRUE) + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("โช MINIMAL THEME\n") +cat("Perfect for: Professional documents, print-friendly diagrams\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Minimal Theme", + theme = "minimal", + node_labels = "both") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ฏ VIRIDIS THEME (Colorblind-Safe)\n") +cat("Perfect for: Accessibility, general use, perceptually uniform\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Viridis Theme", + theme = "viridis") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ฅ MAGMA THEME (Colorblind-Safe, Warm)\n") +cat("Perfect for: High contrast, print materials\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Magma Theme", + theme = "magma") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ PLASMA THEME (Colorblind-Safe, Vibrant)\n") +cat("Perfect for: Presentations, digital displays\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Plasma Theme", + theme = "plasma", + direction = "LR") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐๏ธ CIVIDIS THEME (Maximum Colorblind Accessibility)\n") +cat("Perfect for: Red-green colorblindness (deuteranopia/protanopia)\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow, + title = "Data Processing Pipeline - Cividis Theme", + theme = "cividis") + +cat("\n", paste(rep("=", 50), collapse = ""), "\n") +cat("๐ก USAGE RECOMMENDATIONS\n") +cat(paste(rep("=", 50), collapse = ""), "\n\n") + +cat("๐ Light Theme:\n") +cat(" โข Default theme with bright, friendly colors\n") +cat(" โข Best for documentation websites and light backgrounds\n") +cat(" โข High contrast and readability\n\n") + +cat("๐ Dark Theme:\n") +cat(" โข Muted colors with light text\n") +cat(" โข Perfect for dark mode applications and terminals\n") +cat(" โข Easy on the eyes for extended viewing\n\n") + +cat("๐ Auto Theme:\n") +cat(" โข Uses GitHub's adaptive color system\n") +cat(" โข Automatically looks good in both light and dark modes\n") +cat(" โข Recommended for GitHub README files\n\n") + +cat("โช Minimal Theme:\n") +cat(" โข Grayscale with subtle borders\n") +cat(" โข Professional appearance for business documents\n") +cat(" โข Print-friendly and accessible\n\n") + +cat("๐ฏ Viridis Theme (Colorblind-Safe):\n") +cat(" โข Purple โ Blue โ Green โ Yellow palette\n") +cat(" โข Most widely tested colorblind-safe palette\n") +cat(" โข Perceptually uniform - equal steps appear equally different\n\n") + +cat("๐ฅ Magma Theme (Colorblind-Safe):\n") +cat(" โข Purple โ Red โ Yellow warm palette\n") +cat(" โข High contrast for print and presentations\n") +cat(" โข Works well in grayscale\n\n") + +cat("๐ Plasma Theme (Colorblind-Safe):\n") +cat(" โข Purple โ Pink โ Orange โ Yellow vibrant palette\n") +cat(" โข Bold colors ideal for digital displays\n") +cat(" โข Great for presentations\n\n") + +cat("๐๏ธ Cividis Theme (Maximum Accessibility):\n") +cat(" โข Blue โ Gray โ Yellow palette\n") +cat(" โข Specifically optimized for deuteranopia and protanopia\n") +cat(" โข Avoids red-green entirely\n\n") + +cat("๐ Example Usage:\n") +cat(" # For GitHub README (adapts to user's theme)\n") +cat(" put_diagram(workflow, theme = 'auto')\n\n") +cat(" # For dark documentation sites\n") +cat(" put_diagram(workflow, theme = 'dark', direction = 'LR')\n\n") +cat(" # For professional reports\n") +cat(" put_diagram(workflow, theme = 'minimal', output = 'file')\n\n") +cat(" # For colorblind-safe diagrams\n") +cat(" put_diagram(workflow, theme = 'viridis') # General accessibility\n") +cat(" put_diagram(workflow, theme = 'cividis') # Red-green colorblindness\n\n") + +# Save examples to files for comparison +cat("๐พ Saving theme examples to files...\n") + +themes_to_save <- c("light", "dark", "auto", "minimal", "github", + "viridis", "magma", "plasma", "cividis") +for (theme in themes_to_save) { + filename <- file.path(temp_dir, paste0("workflow_", theme, "_theme.md")) + put_diagram(workflow, + output = "file", + file = filename, + title = paste("Workflow -", stringr::str_to_title(theme), "Theme"), + theme = theme, + show_files = TRUE) +} + +cat("โ Saved theme examples:\n") +for (theme in themes_to_save) { + filename <- paste0("workflow_", theme, "_theme.md") + cat(" โข", filename, "\n") +} + +cat("\n๐ All files saved to:", temp_dir, "\n") +cat("๐ก Try opening these files in different environments to see the themes!\n\n") + cat("๐จ Happy theming with putior! ๐\n") \ No newline at end of file diff --git a/inst/examples/uuid-example.R b/inst/examples/uuid-example.R index 1299913..c728b73 100644 --- a/inst/examples/uuid-example.R +++ b/inst/examples/uuid-example.R @@ -1,124 +1,124 @@ -# Putior UUID Auto-Generation Example -# ============================================================================== -# This example demonstrates how putior automatically generates UUIDs for nodes -# when the id field is omitted from annotations. -# -# To run this example: -# source(system.file("examples", "uuid-example.R", package = "putior")) -# ============================================================================== - -library(putior) - -cat("๐ Putior UUID Auto-Generation Example\n") -cat(paste(rep("=", 50), collapse = ""), "\n\n") - -# Create a sample workflow demonstrating UUID generation -temp_dir <- file.path(tempdir(), "putior_uuid_example") -dir.create(temp_dir, showWarnings = FALSE) - -cat("๐ Creating example files with and without explicit IDs...\n\n") - -# File 1: Annotations without IDs (will get auto-generated UUIDs) -no_ids_script <- c( - "# Script with annotations that omit the id field", - "# put label:\"Load Configuration\", node_type:\"input\", output:\"config.json\"", - "", - "config <- load_config()", - "save_json(config, 'config.json')", - "", - "# put label:\"Process Data\", node_type:\"process\", input:\"config.json\", output:\"processed.csv\"", - "", - "data <- process_with_config('config.json')", - "write.csv(data, 'processed.csv')" -) - -# File 2: Mix of explicit IDs and omitted IDs -mixed_ids_script <- c( - "# Script with some explicit IDs and some omitted", - "# put id:\"fetch_data\", label:\"Fetch Raw Data\", node_type:\"input\", output:\"raw.csv\"", - "", - "raw_data <- fetch_from_api()", - "write.csv(raw_data, 'raw.csv')", - "", - "# This annotation omits the id - will get UUID", - "# put label:\"Validate Data\", node_type:\"process\", input:\"raw.csv\", output:\"validated.csv\"", - "", - "validated <- validate_data(read.csv('raw.csv'))", - "write.csv(validated, 'validated.csv')", - "", - "# put id:\"generate_report\", label:\"Generate Report\", node_type:\"output\", input:\"validated.csv\"" -) - -# File 3: Example with empty ID (will generate warning) -empty_id_script <- c( - "# Script demonstrating empty id warning", - "# put id:\"\", label:\"This will warn\", node_type:\"process\"", - "", - "# Empty IDs generate validation warnings" -) - -# Write files -writeLines(no_ids_script, file.path(temp_dir, "01_no_ids.R")) -writeLines(mixed_ids_script, file.path(temp_dir, "02_mixed_ids.R")) -writeLines(empty_id_script, file.path(temp_dir, "03_empty_id.R")) - -cat("โ Created example files\n\n") - -# Extract workflow with UUID auto-generation -cat("๐ Extracting workflow (UUIDs will be auto-generated)...\n") -cat(paste(rep("-", 50), collapse = ""), "\n") - -# Capture the warning from empty ID -workflow <- suppressWarnings(put(temp_dir)) - -# Show results -cat("\n๐ Extracted nodes:\n") -cat(paste(rep("-", 50), collapse = ""), "\n") - -for (i in seq_len(nrow(workflow))) { - row <- workflow[i, ] - cat(sprintf("%-20s: %s\n", "File", row$file_name)) - cat(sprintf("%-20s: %s\n", "ID", row$id)) - cat(sprintf("%-20s: %s\n", "Label", row$label)) - - # Check if ID looks like a UUID - is_uuid <- grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", - row$id, ignore.case = TRUE) - cat(sprintf("%-20s: %s\n", "Auto-generated?", ifelse(is_uuid, "Yes (UUID)", "No (explicit)"))) - cat(paste(rep("-", 50), collapse = ""), "\n") -} - -# Now extract with validation to see the warning -cat("\nโ ๏ธ Extracting with validation (to show empty ID warning):\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -workflow_validated <- put(temp_dir, validate = TRUE) - -# Generate diagram -cat("\n\n๐จ Generating diagram with auto-generated UUIDs:\n") -cat(paste(rep("-", 50), collapse = ""), "\n") -put_diagram(workflow[workflow$file_name != "03_empty_id.R", ], # Exclude the empty ID example - title = "Workflow with Auto-Generated UUIDs", - node_labels = "label") # Use labels since UUIDs are not human-friendly - -cat("\n\n๐ก KEY POINTS:\n") -cat(paste(rep("=", 50), collapse = ""), "\n") -cat("1. When 'id' is omitted โ UUID is auto-generated\n") -cat("2. When 'id' is explicit โ Your ID is used as-is\n") -cat("3. When 'id' is empty (\"\") โ Validation warning\n") -cat("4. UUIDs ensure uniqueness across workflows\n") -cat("5. Use 'label' for human-readable node names\n\n") - -cat("๐ BEST PRACTICES:\n") -cat("โข Omit 'id' for quick prototyping\n") -cat("โข Use explicit 'id' for stable references\n") -cat("โข Always provide descriptive 'label' values\n") -cat("โข Avoid empty 'id' values\n\n") - -cat("๐๏ธ Example files created in:\n") -cat(" ", temp_dir, "\n\n") - -# Clean up -cat("๐งน Cleaning up temporary files...\n") -unlink(temp_dir, recursive = TRUE) - +# Putior UUID Auto-Generation Example +# ============================================================================== +# This example demonstrates how putior automatically generates UUIDs for nodes +# when the id field is omitted from annotations. +# +# To run this example: +# source(system.file("examples", "uuid-example.R", package = "putior")) +# ============================================================================== + +library(putior) + +cat("๐ Putior UUID Auto-Generation Example\n") +cat(paste(rep("=", 50), collapse = ""), "\n\n") + +# Create a sample workflow demonstrating UUID generation +temp_dir <- file.path(tempdir(), "putior_uuid_example") +dir.create(temp_dir, showWarnings = FALSE) + +cat("๐ Creating example files with and without explicit IDs...\n\n") + +# File 1: Annotations without IDs (will get auto-generated UUIDs) +no_ids_script <- c( + "# Script with annotations that omit the id field", + "# put label:\"Load Configuration\", node_type:\"input\", output:\"config.json\"", + "", + "config <- load_config()", + "save_json(config, 'config.json')", + "", + "# put label:\"Process Data\", node_type:\"process\", input:\"config.json\", output:\"processed.csv\"", + "", + "data <- process_with_config('config.json')", + "write.csv(data, 'processed.csv')" +) + +# File 2: Mix of explicit IDs and omitted IDs +mixed_ids_script <- c( + "# Script with some explicit IDs and some omitted", + "# put id:\"fetch_data\", label:\"Fetch Raw Data\", node_type:\"input\", output:\"raw.csv\"", + "", + "raw_data <- fetch_from_api()", + "write.csv(raw_data, 'raw.csv')", + "", + "# This annotation omits the id - will get UUID", + "# put label:\"Validate Data\", node_type:\"process\", input:\"raw.csv\", output:\"validated.csv\"", + "", + "validated <- validate_data(read.csv('raw.csv'))", + "write.csv(validated, 'validated.csv')", + "", + "# put id:\"generate_report\", label:\"Generate Report\", node_type:\"output\", input:\"validated.csv\"" +) + +# File 3: Example with empty ID (will generate warning) +empty_id_script <- c( + "# Script demonstrating empty id warning", + "# put id:\"\", label:\"This will warn\", node_type:\"process\"", + "", + "# Empty IDs generate validation warnings" +) + +# Write files +writeLines(no_ids_script, file.path(temp_dir, "01_no_ids.R")) +writeLines(mixed_ids_script, file.path(temp_dir, "02_mixed_ids.R")) +writeLines(empty_id_script, file.path(temp_dir, "03_empty_id.R")) + +cat("โ Created example files\n\n") + +# Extract workflow with UUID auto-generation +cat("๐ Extracting workflow (UUIDs will be auto-generated)...\n") +cat(paste(rep("-", 50), collapse = ""), "\n") + +# Capture the warning from empty ID +workflow <- suppressWarnings(put(temp_dir)) + +# Show results +cat("\n๐ Extracted nodes:\n") +cat(paste(rep("-", 50), collapse = ""), "\n") + +for (i in seq_len(nrow(workflow))) { + row <- workflow[i, ] + cat(sprintf("%-20s: %s\n", "File", row$file_name)) + cat(sprintf("%-20s: %s\n", "ID", row$id)) + cat(sprintf("%-20s: %s\n", "Label", row$label)) + + # Check if ID looks like a UUID + is_uuid <- grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + row$id, ignore.case = TRUE) + cat(sprintf("%-20s: %s\n", "Auto-generated?", ifelse(is_uuid, "Yes (UUID)", "No (explicit)"))) + cat(paste(rep("-", 50), collapse = ""), "\n") +} + +# Now extract with validation to see the warning +cat("\nโ ๏ธ Extracting with validation (to show empty ID warning):\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +workflow_validated <- put(temp_dir, validate = TRUE) + +# Generate diagram +cat("\n\n๐จ Generating diagram with auto-generated UUIDs:\n") +cat(paste(rep("-", 50), collapse = ""), "\n") +put_diagram(workflow[workflow$file_name != "03_empty_id.R", ], # Exclude the empty ID example + title = "Workflow with Auto-Generated UUIDs", + node_labels = "label") # Use labels since UUIDs are not human-friendly + +cat("\n\n๐ก KEY POINTS:\n") +cat(paste(rep("=", 50), collapse = ""), "\n") +cat("1. When 'id' is omitted โ UUID is auto-generated\n") +cat("2. When 'id' is explicit โ Your ID is used as-is\n") +cat("3. When 'id' is empty (\"\") โ Validation warning\n") +cat("4. UUIDs ensure uniqueness across workflows\n") +cat("5. Use 'label' for human-readable node names\n\n") + +cat("๐ BEST PRACTICES:\n") +cat("โข Omit 'id' for quick prototyping\n") +cat("โข Use explicit 'id' for stable references\n") +cat("โข Always provide descriptive 'label' values\n") +cat("โข Avoid empty 'id' values\n\n") + +cat("๐๏ธ Example files created in:\n") +cat(" ", temp_dir, "\n\n") + +# Clean up +cat("๐งน Cleaning up temporary files...\n") +unlink(temp_dir, recursive = TRUE) + cat("\nโ UUID auto-generation example complete!\n") \ No newline at end of file diff --git a/inst/examples/variable-reference-example.R b/inst/examples/variable-reference-example.R index 3d547db..bb27193 100644 --- a/inst/examples/variable-reference-example.R +++ b/inst/examples/variable-reference-example.R @@ -1,269 +1,269 @@ -# Variable Reference Example for putior -# ============================================================================== -# This example demonstrates how to reference variables and objects within -# putior workflow diagrams using the "internal" file extension pattern. -# This technique allows tracking of non-persistent objects that exist only -# during script execution. -# -# Problem: How to reference a variable created by a process as a label in -# a put comment or track objects that don't persist to disk? -# -# Solution: Use the "internal" file extension to indicate non-persistent -# outputs that represent variables, objects, or temporary results. -# -# Source: GitHub issue #2 - Response by @pjt222 (package maintainer) -# To run this example: -# source(system.file("examples", "variable-reference-example.R", package = "putior")) -# ============================================================================== - -library(putior) - -# Create temporary directory for example -temp_dir <- file.path(tempdir(), "putior_variable_reference") -dir.create(temp_dir, showWarnings = FALSE) - -cat("Creating variable reference example in:", temp_dir, "\n") - -# ============================================================================== -# EXAMPLE 1: BASIC VARIABLE TRACKING -# ============================================================================== - -# Script 1: Create a variable and save it -script1_content <- c( - "# Variable Creation Script", - "# Demonstrates: Using 'internal' extension to track variables", - "# put id:'create_var', label:'Create Variable', node_type:'start', output:'var.internal, var.RData'", - "", - "# Create a variable (exists in memory)", - "var <- letters[1:5]", - "print(paste('Variable created:', paste(var, collapse=', ')))", - "", - "# Also save to disk for persistence (optional)", - "save(var, file = 'var.RData')", - "", - "# The .internal extension tells putior this represents a variable/object", - "# that exists during execution, while .RData is the persistent file" -) - -# Script 2: Use the variable from Script 1 -script2_content <- c( - "# Variable Processing Script", - "# Demonstrates: Loading persisted data and creating new variables", - "# put id:'process_var', label:'Process Variable', node_type:'process', input:'var.RData', output:'processed_var.internal, result.txt'", - "", - "# Load the variable if working with persistent version", - "load('var.RData')", - "", - "# Process the variable", - "processed_var <- toupper(var)", - "print(paste('Processed variable:', paste(processed_var, collapse=', ')))", - "", - "# Save result to file", - "writeLines(processed_var, 'result.txt')", - "", - "# processed_var.internal represents the new variable created in this script", - "# var.internal existed only in the previous script's memory" -) - -# Write the scripts -writeLines(script1_content, file.path(temp_dir, "01_create_variable.R")) -writeLines(script2_content, file.path(temp_dir, "02_process_variable.R")) - -# ============================================================================== -# EXAMPLE 2: EXTEND THE PIPELINE - BUILD ON PREVIOUS RESULTS -# ============================================================================== - -# Script 3: Further processing using results from Script 2 -script3_content <- c( - "# Advanced Processing Script", - "# put id:'advanced_process', label:'Advanced Data Processing', node_type:'process', input:'result.txt', output:'enhanced_data.internal, enhanced_results.csv'", - "", - "# Read results from previous script", - "processed_text <- readLines('result.txt')", - "", - "# Create enhanced data based on previous results", - "enhanced_data <- data.frame(", - " original = processed_text,", - " length = nchar(processed_text),", - " reversed = sapply(processed_text, function(x) paste(rev(strsplit(x, '')[[1]]), collapse = ''))", - ")", - "", - "# Save enhanced results", - "write.csv(enhanced_data, 'enhanced_results.csv', row.names = FALSE)", - "", - "print('Enhanced processing completed')" -) - -# Script 4: Final analysis using all previous outputs -script4_content <- c( - "# Final Analysis Script", - "# put id:'final_analysis', label:'Final Analysis & Report', node_type:'output', input:'var.RData, enhanced_results.csv', output:'final_report.internal, complete_analysis.txt'", - "", - "# Load original variable", - "load('var.RData')", - "", - "# Load enhanced results", - "enhanced_data <- read.csv('enhanced_results.csv')", - "", - "# Create comprehensive final report (in-memory object)", - "final_report <- list(", - " original_var_length = length(var),", - " enhanced_data_rows = nrow(enhanced_data),", - " processing_summary = paste('Processed', length(var), 'original items into', nrow(enhanced_data), 'enhanced records')", - ")", - "", - "# Save final analysis", - "writeLines(capture.output(str(final_report)), 'complete_analysis.txt')", - "", - "print('Final analysis completed')" -) - -writeLines(script3_content, file.path(temp_dir, "03_advanced_process.R")) -writeLines(script4_content, file.path(temp_dir, "04_final_analysis.R")) - -cat("Created", length(list.files(temp_dir, pattern = "*.R")), "example scripts\n") - -# ============================================================================== -# EXTRACT WORKFLOW WITH PUTIOR -# ============================================================================== - -cat("\n=== Extracting workflow with putior::put() ===\n") -workflow <- put(temp_dir) - -# Display results -cat("\nVariable Reference Workflow Results:\n") -print(workflow) - -# ============================================================================== -# ANALYZE VARIABLE TRACKING -# ============================================================================== - -if (nrow(workflow) > 0) { - - cat("\n=== Variable Tracking Analysis ===\n") - - # Find .internal files (variables/objects) - all_outputs <- unlist(strsplit(workflow$output[!is.na(workflow$output)], ", ")) - all_inputs <- unlist(strsplit(workflow$input[!is.na(workflow$input)], ", ")) - - internal_outputs <- all_outputs[grepl("\\.internal$", all_outputs)] - internal_inputs <- all_inputs[grepl("\\.internal$", all_inputs)] - - cat("\nVariable/Object Tracking:\n") - cat(" Variables Created:", length(unique(internal_outputs)), "\n") - cat(" Variables Used:", length(unique(internal_inputs)), "\n") - - if (length(internal_outputs) > 0) { - cat(" \nVariables/Objects in Workflow:\n") - for (var in unique(internal_outputs)) { - var_name <- gsub("\\.internal$", "", var) - cat(" -", var_name, "\n") - } - } - - # Find persistent files - persistent_outputs <- all_outputs[!grepl("\\.internal$", all_outputs)] - persistent_inputs <- all_inputs[!grepl("\\.internal$", all_inputs)] - - cat("\nPersistent Files:\n") - cat(" Files Created:", length(unique(persistent_outputs)), "\n") - cat(" Files Consumed:", length(unique(persistent_inputs)), "\n") - - if (length(persistent_outputs) > 0) { - cat(" \nPersistent Files in Workflow:\n") - for (file in unique(persistent_outputs)) { - cat(" -", file, "\n") - } - } - - # Show data flow - cat("\n=== Data Flow Summary ===\n") - for (i in 1:nrow(workflow)) { - row <- workflow[i, ] - cat("Step", i, ":", row$label, "\n") - - if (!is.na(row$input)) { - inputs <- unlist(strsplit(row$input, ", ")) - var_inputs <- inputs[grepl("\\.internal$", inputs)] - file_inputs <- inputs[!grepl("\\.internal$", inputs)] - - if (length(var_inputs) > 0) { - cat(" Uses variables:", paste(gsub("\\.internal$", "", var_inputs), collapse = ", "), "\n") - } - if (length(file_inputs) > 0) { - cat(" Reads files:", paste(file_inputs, collapse = ", "), "\n") - } - } - - if (!is.na(row$output)) { - outputs <- unlist(strsplit(row$output, ", ")) - var_outputs <- outputs[grepl("\\.internal$", outputs)] - file_outputs <- outputs[!grepl("\\.internal$", outputs)] - - if (length(var_outputs) > 0) { - cat(" Creates variables:", paste(gsub("\\.internal$", "", var_outputs), collapse = ", "), "\n") - } - if (length(file_outputs) > 0) { - cat(" Writes files:", paste(file_outputs, collapse = ", "), "\n") - } - } - cat("\n") - } - -} else { - cat("No workflow annotations found. Check PUT annotation syntax.\n") -} - -# ============================================================================== -# KEY INSIGHTS AND BEST PRACTICES -# ============================================================================== - -cat("=== Key Insights for Variable References ===\n") -cat("\n1. Use '.internal' Extension for Variables:\n") -cat(" - Append '.internal' to variable/object names\n") -cat(" - Example: 'my_variable.internal'\n") -cat(" - This tells putior the item represents an in-memory object\n") - -cat("\n2. Combine with Persistent Files:\n") -cat(" - Use both .internal and file outputs when appropriate\n") -cat(" - Example: output:'data.internal, data.RData'\n") -cat(" - .internal tracks the variable, .RData tracks the saved file\n") - -cat("\n3. .internal Variables Are Script-Local:\n") -cat(" - .internal variables exist only during script execution\n") -cat(" - They CANNOT be used as inputs to other scripts\n") -cat(" - Use persistent files (RData, CSV, etc.) for inter-script data flow\n") - -cat("\n4. Multiple Variables in One Step:\n") -cat(" - List multiple .internal items: 'var1.internal, var2.internal'\n") -cat(" - Useful for scripts that create multiple objects\n") - -cat("\n5. Document Variable Purpose:\n") -cat(" - Use descriptive names: 'cleaned_data.internal'\n") -cat(" - The variable name (before .internal) shows what was created\n") -cat(" - Helps document the computational steps within each script\n") - -# ============================================================================== -# CLEANUP -# ============================================================================== - -cat("\n=== Example Complete ===\n") -cat("Example files location:", temp_dir, "\n") -cat("\nTo explore further:\n") -cat("1. Examine generated scripts to see .internal usage patterns\n") -cat("2. Modify scripts to track your own variables\n") -cat("3. Try generating a diagram with put_diagram() to visualize\n") - -# Optional cleanup -cat("\nClean up temporary files? (y/n): ") -if (interactive()) { - response <- readline() - if (tolower(response) == "y") { - unlink(temp_dir, recursive = TRUE) - cat("Temporary files removed.\n") - } else { - cat("Temporary files preserved for inspection.\n") - } -} else { - cat("(Running non-interactively - files preserved)\n") +# Variable Reference Example for putior +# ============================================================================== +# This example demonstrates how to reference variables and objects within +# putior workflow diagrams using the "internal" file extension pattern. +# This technique allows tracking of non-persistent objects that exist only +# during script execution. +# +# Problem: How to reference a variable created by a process as a label in +# a put comment or track objects that don't persist to disk? +# +# Solution: Use the "internal" file extension to indicate non-persistent +# outputs that represent variables, objects, or temporary results. +# +# Source: GitHub issue #2 - Response by @pjt222 (package maintainer) +# To run this example: +# source(system.file("examples", "variable-reference-example.R", package = "putior")) +# ============================================================================== + +library(putior) + +# Create temporary directory for example +temp_dir <- file.path(tempdir(), "putior_variable_reference") +dir.create(temp_dir, showWarnings = FALSE) + +cat("Creating variable reference example in:", temp_dir, "\n") + +# ============================================================================== +# EXAMPLE 1: BASIC VARIABLE TRACKING +# ============================================================================== + +# Script 1: Create a variable and save it +script1_content <- c( + "# Variable Creation Script", + "# Demonstrates: Using 'internal' extension to track variables", + "# put id:'create_var', label:'Create Variable', node_type:'start', output:'var.internal, var.RData'", + "", + "# Create a variable (exists in memory)", + "var <- letters[1:5]", + "print(paste('Variable created:', paste(var, collapse=', ')))", + "", + "# Also save to disk for persistence (optional)", + "save(var, file = 'var.RData')", + "", + "# The .internal extension tells putior this represents a variable/object", + "# that exists during execution, while .RData is the persistent file" +) + +# Script 2: Use the variable from Script 1 +script2_content <- c( + "# Variable Processing Script", + "# Demonstrates: Loading persisted data and creating new variables", + "# put id:'process_var', label:'Process Variable', node_type:'process', input:'var.RData', output:'processed_var.internal, result.txt'", + "", + "# Load the variable if working with persistent version", + "load('var.RData')", + "", + "# Process the variable", + "processed_var <- toupper(var)", + "print(paste('Processed variable:', paste(processed_var, collapse=', ')))", + "", + "# Save result to file", + "writeLines(processed_var, 'result.txt')", + "", + "# processed_var.internal represents the new variable created in this script", + "# var.internal existed only in the previous script's memory" +) + +# Write the scripts +writeLines(script1_content, file.path(temp_dir, "01_create_variable.R")) +writeLines(script2_content, file.path(temp_dir, "02_process_variable.R")) + +# ============================================================================== +# EXAMPLE 2: EXTEND THE PIPELINE - BUILD ON PREVIOUS RESULTS +# ============================================================================== + +# Script 3: Further processing using results from Script 2 +script3_content <- c( + "# Advanced Processing Script", + "# put id:'advanced_process', label:'Advanced Data Processing', node_type:'process', input:'result.txt', output:'enhanced_data.internal, enhanced_results.csv'", + "", + "# Read results from previous script", + "processed_text <- readLines('result.txt')", + "", + "# Create enhanced data based on previous results", + "enhanced_data <- data.frame(", + " original = processed_text,", + " length = nchar(processed_text),", + " reversed = sapply(processed_text, function(x) paste(rev(strsplit(x, '')[[1]]), collapse = ''))", + ")", + "", + "# Save enhanced results", + "write.csv(enhanced_data, 'enhanced_results.csv', row.names = FALSE)", + "", + "print('Enhanced processing completed')" +) + +# Script 4: Final analysis using all previous outputs +script4_content <- c( + "# Final Analysis Script", + "# put id:'final_analysis', label:'Final Analysis & Report', node_type:'output', input:'var.RData, enhanced_results.csv', output:'final_report.internal, complete_analysis.txt'", + "", + "# Load original variable", + "load('var.RData')", + "", + "# Load enhanced results", + "enhanced_data <- read.csv('enhanced_results.csv')", + "", + "# Create comprehensive final report (in-memory object)", + "final_report <- list(", + " original_var_length = length(var),", + " enhanced_data_rows = nrow(enhanced_data),", + " processing_summary = paste('Processed', length(var), 'original items into', nrow(enhanced_data), 'enhanced records')", + ")", + "", + "# Save final analysis", + "writeLines(capture.output(str(final_report)), 'complete_analysis.txt')", + "", + "print('Final analysis completed')" +) + +writeLines(script3_content, file.path(temp_dir, "03_advanced_process.R")) +writeLines(script4_content, file.path(temp_dir, "04_final_analysis.R")) + +cat("Created", length(list.files(temp_dir, pattern = "*.R")), "example scripts\n") + +# ============================================================================== +# EXTRACT WORKFLOW WITH PUTIOR +# ============================================================================== + +cat("\n=== Extracting workflow with putior::put() ===\n") +workflow <- put(temp_dir) + +# Display results +cat("\nVariable Reference Workflow Results:\n") +print(workflow) + +# ============================================================================== +# ANALYZE VARIABLE TRACKING +# ============================================================================== + +if (nrow(workflow) > 0) { + + cat("\n=== Variable Tracking Analysis ===\n") + + # Find .internal files (variables/objects) + all_outputs <- unlist(strsplit(workflow$output[!is.na(workflow$output)], ", ")) + all_inputs <- unlist(strsplit(workflow$input[!is.na(workflow$input)], ", ")) + + internal_outputs <- all_outputs[grepl("\\.internal$", all_outputs)] + internal_inputs <- all_inputs[grepl("\\.internal$", all_inputs)] + + cat("\nVariable/Object Tracking:\n") + cat(" Variables Created:", length(unique(internal_outputs)), "\n") + cat(" Variables Used:", length(unique(internal_inputs)), "\n") + + if (length(internal_outputs) > 0) { + cat(" \nVariables/Objects in Workflow:\n") + for (var in unique(internal_outputs)) { + var_name <- gsub("\\.internal$", "", var) + cat(" -", var_name, "\n") + } + } + + # Find persistent files + persistent_outputs <- all_outputs[!grepl("\\.internal$", all_outputs)] + persistent_inputs <- all_inputs[!grepl("\\.internal$", all_inputs)] + + cat("\nPersistent Files:\n") + cat(" Files Created:", length(unique(persistent_outputs)), "\n") + cat(" Files Consumed:", length(unique(persistent_inputs)), "\n") + + if (length(persistent_outputs) > 0) { + cat(" \nPersistent Files in Workflow:\n") + for (file in unique(persistent_outputs)) { + cat(" -", file, "\n") + } + } + + # Show data flow + cat("\n=== Data Flow Summary ===\n") + for (i in 1:nrow(workflow)) { + row <- workflow[i, ] + cat("Step", i, ":", row$label, "\n") + + if (!is.na(row$input)) { + inputs <- unlist(strsplit(row$input, ", ")) + var_inputs <- inputs[grepl("\\.internal$", inputs)] + file_inputs <- inputs[!grepl("\\.internal$", inputs)] + + if (length(var_inputs) > 0) { + cat(" Uses variables:", paste(gsub("\\.internal$", "", var_inputs), collapse = ", "), "\n") + } + if (length(file_inputs) > 0) { + cat(" Reads files:", paste(file_inputs, collapse = ", "), "\n") + } + } + + if (!is.na(row$output)) { + outputs <- unlist(strsplit(row$output, ", ")) + var_outputs <- outputs[grepl("\\.internal$", outputs)] + file_outputs <- outputs[!grepl("\\.internal$", outputs)] + + if (length(var_outputs) > 0) { + cat(" Creates variables:", paste(gsub("\\.internal$", "", var_outputs), collapse = ", "), "\n") + } + if (length(file_outputs) > 0) { + cat(" Writes files:", paste(file_outputs, collapse = ", "), "\n") + } + } + cat("\n") + } + +} else { + cat("No workflow annotations found. Check PUT annotation syntax.\n") +} + +# ============================================================================== +# KEY INSIGHTS AND BEST PRACTICES +# ============================================================================== + +cat("=== Key Insights for Variable References ===\n") +cat("\n1. Use '.internal' Extension for Variables:\n") +cat(" - Append '.internal' to variable/object names\n") +cat(" - Example: 'my_variable.internal'\n") +cat(" - This tells putior the item represents an in-memory object\n") + +cat("\n2. Combine with Persistent Files:\n") +cat(" - Use both .internal and file outputs when appropriate\n") +cat(" - Example: output:'data.internal, data.RData'\n") +cat(" - .internal tracks the variable, .RData tracks the saved file\n") + +cat("\n3. .internal Variables Are Script-Local:\n") +cat(" - .internal variables exist only during script execution\n") +cat(" - They CANNOT be used as inputs to other scripts\n") +cat(" - Use persistent files (RData, CSV, etc.) for inter-script data flow\n") + +cat("\n4. Multiple Variables in One Step:\n") +cat(" - List multiple .internal items: 'var1.internal, var2.internal'\n") +cat(" - Useful for scripts that create multiple objects\n") + +cat("\n5. Document Variable Purpose:\n") +cat(" - Use descriptive names: 'cleaned_data.internal'\n") +cat(" - The variable name (before .internal) shows what was created\n") +cat(" - Helps document the computational steps within each script\n") + +# ============================================================================== +# CLEANUP +# ============================================================================== + +cat("\n=== Example Complete ===\n") +cat("Example files location:", temp_dir, "\n") +cat("\nTo explore further:\n") +cat("1. Examine generated scripts to see .internal usage patterns\n") +cat("2. Modify scripts to track your own variables\n") +cat("3. Try generating a diagram with put_diagram() to visualize\n") + +# Optional cleanup +cat("\nClean up temporary files? (y/n): ") +if (interactive()) { + response <- readline() + if (tolower(response) == "y") { + unlink(temp_dir, recursive = TRUE) + cat("Temporary files removed.\n") + } else { + cat("Temporary files preserved for inspection.\n") + } +} else { + cat("(Running non-interactively - files preserved)\n") } \ No newline at end of file diff --git a/inst/hex-sticker/generated/create-hex.R b/inst/hex-sticker/generated/create-hex.R index 443ed17..76c8033 100644 --- a/inst/hex-sticker/generated/create-hex.R +++ b/inst/hex-sticker/generated/create-hex.R @@ -1,260 +1,260 @@ -# Reproducible Hex Sticker Generation for putior -# -# This script recreates the putior hex sticker design using pure ggplot2 -# for maximum reproducibility and maintainability. - -library(ggplot2) -library(grid) - -#' Create the putior hex sticker -#' -#' Generates the putior package hex sticker with all design elements: -#' - Purple gradient background -#' - Document icons with "# put" annotation -#' - Network visualization with connected nodes -#' - Package name typography -#' -#' @param output_path Path where to save the hex sticker (PNG format) -#' @param width Width in pixels (default: 400) -#' @param height Height in pixels (default: 400) -#' @param dpi DPI for output (default: 300) -#' @return Invisible ggplot object -create_putior_hex <- function(output_path = "putior-hex.png", - width = 400, - height = 400, - dpi = 300) { - - # Hex sticker dimensions and positioning - hex_size <- 1.0 - center_x <- 0 - center_y <- 0 - - # Create hexagon coordinates - create_hexagon <- function(center_x = 0, center_y = 0, size = 1) { - angles <- seq(0, 2*pi, length.out = 7) - data.frame( - x = center_x + size * cos(angles), - y = center_y + size * sin(angles) - ) - } - - hex_coords <- create_hexagon(center_x, center_y, hex_size) - - # Color palette (from SVG: rgb(187, 173, 225) to rgb(0, 0, 0)) - colors <- list( - bg_light = "#BBADE1", # Light purple (top) - exact SVG color - bg_dark = "#6B5B95", # Mid purple - bg_black = "#000000", # Black (bottom) - exact SVG color - border = "#000000", # Black border (exact SVG) - white = "#FFFFFF", # White for text and elements - gray = "#8C8C8C" # Gray for "# put" text (exact SVG) - ) - - # Create base plot - p <- ggplot() + - # Set coordinate system and limits - coord_fixed(ratio = 1) + - xlim(-1.3, 1.3) + - ylim(-1.3, 1.3) + - theme_void() + - theme( - plot.background = element_rect(fill = "transparent", color = NA), - panel.background = element_rect(fill = "transparent", color = NA), - plot.margin = margin(0, 0, 0, 0) - ) - - # Add gradient background using multiple layers - gradient_layers <- 15 - for (i in 1:gradient_layers) { - alpha_val <- 0.15 - y_offset <- (i - gradient_layers/2) * 0.15 - - # Interpolate color from light to dark based on position - color_mix <- i / gradient_layers - if (color_mix < 0.3) { - fill_color <- colors$bg_light - } else if (color_mix < 0.7) { - fill_color <- colors$bg_dark - } else { - fill_color <- colors$bg_black - } - - p <- p + - geom_polygon( - data = hex_coords, - aes(x = x, y = y + y_offset * 0.1), - fill = fill_color, - alpha = alpha_val, - color = NA - ) - } - - # Add main hexagon border - p <- p + - geom_polygon( - data = hex_coords, - aes(x = x, y = y), - fill = NA, - color = colors$border, - linewidth = 2 - ) - - # Document stack coordinates (based on SVG: x="150" y="206" width="100" height="120") - # Converting SVG coordinates to our coordinate system - doc_base_x <- -0.55 - doc_base_y <- -0.05 - doc_width <- 0.32 - doc_height <- 0.42 - - # Create document icons (3 stacked papers with exact SVG positioning) - # SVG has documents at relative positions: (0,20), (10,10), (20,0) - docs_data <- data.frame( - xmin = c(doc_base_x, doc_base_x + 0.04, doc_base_x + 0.08), - xmax = c(doc_base_x + doc_width, doc_base_x + doc_width + 0.04, doc_base_x + doc_width + 0.08), - ymin = c(doc_base_y + 0.08, doc_base_y + 0.04, doc_base_y), - ymax = c(doc_base_y + doc_height + 0.08, doc_base_y + doc_height + 0.04, doc_base_y + doc_height), - layer = c(1, 2, 3) - ) - - # Add document rectangles (back to front) - for (i in 3:1) { - doc <- docs_data[docs_data$layer == i, ] - alpha_val <- c(0.7, 0.8, 1.0)[i] - - p <- p + - geom_rect( - data = doc, - aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), - fill = colors$white, - color = colors$border, - alpha = alpha_val, - linewidth = 0.5 - ) - } - - # Add "# put" text on the front document (SVG: x="10" y="70" within group) - p <- p + - annotate("text", - x = doc_base_x + 0.14, - y = doc_base_y + 0.27, - label = "# put", - size = 2.0, - color = colors$gray, - family = "mono", - fontface = "bold") - - # Network visualization (right side - based on SVG group at x="196" y="136") - # Converting SVG node positions to our coordinate system - # Main large node at (0,10) 40x40, others positioned relative to this - nodes <- data.frame( - x = c(0.08, 0.28, 0.48, 0.43, 0.58, 0.35, 0.65, 0.5), - y = c(0.35, 0.42, 0.17, 0.3, 0.1, 0.1, 0.0, -0.07), - size = c(4.0, 2.0, 1.6, 3.0, 1.0, 1.8, 1.4, 2.2), - id = 1:8 - ) - - # Connection data for network edges (based on SVG edge definitions) - edges <- data.frame( - x1 = c(0.08, 0.08, 0.28, 0.48, 0.43, 0.43, 0.43), - y1 = c(0.35, 0.35, 0.42, 0.17, 0.3, 0.3, 0.3), - x2 = c(0.28, 0.35, 0.48, 0.35, 0.65, 0.58, 0.5), - y2 = c(0.42, 0.1, 0.17, 0.1, 0.0, 0.1, -0.07) - ) - - # Add network edges - p <- p + - geom_segment( - data = edges, - aes(x = x1, y = y1, xend = x2, yend = y2), - color = colors$white, - linewidth = 1.2, - alpha = 0.8 - ) - - # Add network nodes - p <- p + - geom_point( - data = nodes, - aes(x = x, y = y, size = size), - color = colors$white, - fill = colors$white, - shape = 21, - stroke = 1.5, - alpha = 0.9 - ) + - scale_size_identity() - - # Add package name "putior" (SVG: x="250" y="306" rotation=330) - p <- p + - annotate("text", - x = 0.15, - y = -0.55, - label = "putior", - size = 11, - color = colors$white, - family = "sans", - fontface = "bold", - angle = 330) - - # Save the plot - ggsave( - filename = output_path, - plot = p, - width = width/dpi, - height = height/dpi, - dpi = dpi, - bg = "transparent" - ) - - # Also save as SVG for comparison (same dimensions as your original) - svg_path <- sub("\\.png$", ".svg", output_path) - ggsave( - filename = svg_path, - plot = p, - width = 399/72, # Convert pixels to inches (399px from your SVG) - height = 384/72, # Convert pixels to inches (384px from your SVG) - bg = "transparent", - device = "svg" - ) - - cat("โจ Hex sticker saved to:", output_path, "\n") - - invisible(p) -} - -#' Generate all hex sticker variants -#' -#' Creates multiple versions of the hex sticker for different use cases -generate_hex_variants <- function(base_dir = ".") { - - # High-res version for print/packaging - create_putior_hex( - output_path = file.path(base_dir, "putior-hex-large.png"), - width = 800, - height = 800, - dpi = 300 - ) - - # Standard web version - create_putior_hex( - output_path = file.path(base_dir, "putior-hex.png"), - width = 400, - height = 400, - dpi = 150 - ) - - # Small version for badges/icons - create_putior_hex( - output_path = file.path(base_dir, "putior-hex-small.png"), - width = 200, - height = 200, - dpi = 100 - ) - - cat("๐จ Generated all hex sticker variants!\n") -} - -# If script is run directly, generate the hex stickers -if (!interactive()) { - generate_hex_variants() +# Reproducible Hex Sticker Generation for putior +# +# This script recreates the putior hex sticker design using pure ggplot2 +# for maximum reproducibility and maintainability. + +library(ggplot2) +library(grid) + +#' Create the putior hex sticker +#' +#' Generates the putior package hex sticker with all design elements: +#' - Purple gradient background +#' - Document icons with "# put" annotation +#' - Network visualization with connected nodes +#' - Package name typography +#' +#' @param output_path Path where to save the hex sticker (PNG format) +#' @param width Width in pixels (default: 400) +#' @param height Height in pixels (default: 400) +#' @param dpi DPI for output (default: 300) +#' @return Invisible ggplot object +create_putior_hex <- function(output_path = "putior-hex.png", + width = 400, + height = 400, + dpi = 300) { + + # Hex sticker dimensions and positioning + hex_size <- 1.0 + center_x <- 0 + center_y <- 0 + + # Create hexagon coordinates + create_hexagon <- function(center_x = 0, center_y = 0, size = 1) { + angles <- seq(0, 2*pi, length.out = 7) + data.frame( + x = center_x + size * cos(angles), + y = center_y + size * sin(angles) + ) + } + + hex_coords <- create_hexagon(center_x, center_y, hex_size) + + # Color palette (from SVG: rgb(187, 173, 225) to rgb(0, 0, 0)) + colors <- list( + bg_light = "#BBADE1", # Light purple (top) - exact SVG color + bg_dark = "#6B5B95", # Mid purple + bg_black = "#000000", # Black (bottom) - exact SVG color + border = "#000000", # Black border (exact SVG) + white = "#FFFFFF", # White for text and elements + gray = "#8C8C8C" # Gray for "# put" text (exact SVG) + ) + + # Create base plot + p <- ggplot() + + # Set coordinate system and limits + coord_fixed(ratio = 1) + + xlim(-1.3, 1.3) + + ylim(-1.3, 1.3) + + theme_void() + + theme( + plot.background = element_rect(fill = "transparent", color = NA), + panel.background = element_rect(fill = "transparent", color = NA), + plot.margin = margin(0, 0, 0, 0) + ) + + # Add gradient background using multiple layers + gradient_layers <- 15 + for (i in 1:gradient_layers) { + alpha_val <- 0.15 + y_offset <- (i - gradient_layers/2) * 0.15 + + # Interpolate color from light to dark based on position + color_mix <- i / gradient_layers + if (color_mix < 0.3) { + fill_color <- colors$bg_light + } else if (color_mix < 0.7) { + fill_color <- colors$bg_dark + } else { + fill_color <- colors$bg_black + } + + p <- p + + geom_polygon( + data = hex_coords, + aes(x = x, y = y + y_offset * 0.1), + fill = fill_color, + alpha = alpha_val, + color = NA + ) + } + + # Add main hexagon border + p <- p + + geom_polygon( + data = hex_coords, + aes(x = x, y = y), + fill = NA, + color = colors$border, + linewidth = 2 + ) + + # Document stack coordinates (based on SVG: x="150" y="206" width="100" height="120") + # Converting SVG coordinates to our coordinate system + doc_base_x <- -0.55 + doc_base_y <- -0.05 + doc_width <- 0.32 + doc_height <- 0.42 + + # Create document icons (3 stacked papers with exact SVG positioning) + # SVG has documents at relative positions: (0,20), (10,10), (20,0) + docs_data <- data.frame( + xmin = c(doc_base_x, doc_base_x + 0.04, doc_base_x + 0.08), + xmax = c(doc_base_x + doc_width, doc_base_x + doc_width + 0.04, doc_base_x + doc_width + 0.08), + ymin = c(doc_base_y + 0.08, doc_base_y + 0.04, doc_base_y), + ymax = c(doc_base_y + doc_height + 0.08, doc_base_y + doc_height + 0.04, doc_base_y + doc_height), + layer = c(1, 2, 3) + ) + + # Add document rectangles (back to front) + for (i in 3:1) { + doc <- docs_data[docs_data$layer == i, ] + alpha_val <- c(0.7, 0.8, 1.0)[i] + + p <- p + + geom_rect( + data = doc, + aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), + fill = colors$white, + color = colors$border, + alpha = alpha_val, + linewidth = 0.5 + ) + } + + # Add "# put" text on the front document (SVG: x="10" y="70" within group) + p <- p + + annotate("text", + x = doc_base_x + 0.14, + y = doc_base_y + 0.27, + label = "# put", + size = 2.0, + color = colors$gray, + family = "mono", + fontface = "bold") + + # Network visualization (right side - based on SVG group at x="196" y="136") + # Converting SVG node positions to our coordinate system + # Main large node at (0,10) 40x40, others positioned relative to this + nodes <- data.frame( + x = c(0.08, 0.28, 0.48, 0.43, 0.58, 0.35, 0.65, 0.5), + y = c(0.35, 0.42, 0.17, 0.3, 0.1, 0.1, 0.0, -0.07), + size = c(4.0, 2.0, 1.6, 3.0, 1.0, 1.8, 1.4, 2.2), + id = 1:8 + ) + + # Connection data for network edges (based on SVG edge definitions) + edges <- data.frame( + x1 = c(0.08, 0.08, 0.28, 0.48, 0.43, 0.43, 0.43), + y1 = c(0.35, 0.35, 0.42, 0.17, 0.3, 0.3, 0.3), + x2 = c(0.28, 0.35, 0.48, 0.35, 0.65, 0.58, 0.5), + y2 = c(0.42, 0.1, 0.17, 0.1, 0.0, 0.1, -0.07) + ) + + # Add network edges + p <- p + + geom_segment( + data = edges, + aes(x = x1, y = y1, xend = x2, yend = y2), + color = colors$white, + linewidth = 1.2, + alpha = 0.8 + ) + + # Add network nodes + p <- p + + geom_point( + data = nodes, + aes(x = x, y = y, size = size), + color = colors$white, + fill = colors$white, + shape = 21, + stroke = 1.5, + alpha = 0.9 + ) + + scale_size_identity() + + # Add package name "putior" (SVG: x="250" y="306" rotation=330) + p <- p + + annotate("text", + x = 0.15, + y = -0.55, + label = "putior", + size = 11, + color = colors$white, + family = "sans", + fontface = "bold", + angle = 330) + + # Save the plot + ggsave( + filename = output_path, + plot = p, + width = width/dpi, + height = height/dpi, + dpi = dpi, + bg = "transparent" + ) + + # Also save as SVG for comparison (same dimensions as your original) + svg_path <- sub("\\.png$", ".svg", output_path) + ggsave( + filename = svg_path, + plot = p, + width = 399/72, # Convert pixels to inches (399px from your SVG) + height = 384/72, # Convert pixels to inches (384px from your SVG) + bg = "transparent", + device = "svg" + ) + + cat("โจ Hex sticker saved to:", output_path, "\n") + + invisible(p) +} + +#' Generate all hex sticker variants +#' +#' Creates multiple versions of the hex sticker for different use cases +generate_hex_variants <- function(base_dir = ".") { + + # High-res version for print/packaging + create_putior_hex( + output_path = file.path(base_dir, "putior-hex-large.png"), + width = 800, + height = 800, + dpi = 300 + ) + + # Standard web version + create_putior_hex( + output_path = file.path(base_dir, "putior-hex.png"), + width = 400, + height = 400, + dpi = 150 + ) + + # Small version for badges/icons + create_putior_hex( + output_path = file.path(base_dir, "putior-hex-small.png"), + width = 200, + height = 200, + dpi = 100 + ) + + cat("๐จ Generated all hex sticker variants!\n") +} + +# If script is run directly, generate the hex stickers +if (!interactive()) { + generate_hex_variants() } \ No newline at end of file diff --git a/renv/activate.R b/renv/activate.R index 256edab..90b251c 100644 --- a/renv/activate.R +++ b/renv/activate.R @@ -1,1334 +1,1334 @@ - -local({ - - # the requested version of renv - version <- "1.1.4" - attr(version, "sha") <- NULL - - # the project directory - project <- Sys.getenv("RENV_PROJECT") - if (!nzchar(project)) - project <- getwd() - - # use start-up diagnostics if enabled - diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") - if (diagnostics) { - start <- Sys.time() - profile <- tempfile("renv-startup-", fileext = ".Rprof") - utils::Rprof(profile) - on.exit({ - utils::Rprof(NULL) - elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) - writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) - writeLines(sprintf("- Profile: %s", profile)) - print(utils::summaryRprof(profile)) - }, add = TRUE) - } - - # figure out whether the autoloader is enabled - enabled <- local({ - - # first, check config option - override <- getOption("renv.config.autoloader.enabled") - if (!is.null(override)) - return(override) - - # if we're being run in a context where R_LIBS is already set, - # don't load -- presumably we're being run as a sub-process and - # the parent process has already set up library paths for us - rcmd <- Sys.getenv("R_CMD", unset = NA) - rlibs <- Sys.getenv("R_LIBS", unset = NA) - if (!is.na(rlibs) && !is.na(rcmd)) - return(FALSE) - - # next, check environment variables - # prefer using the configuration one in the future - envvars <- c( - "RENV_CONFIG_AUTOLOADER_ENABLED", - "RENV_AUTOLOADER_ENABLED", - "RENV_ACTIVATE_PROJECT" - ) - - for (envvar in envvars) { - envval <- Sys.getenv(envvar, unset = NA) - if (!is.na(envval)) - return(tolower(envval) %in% c("true", "t", "1")) - } - - # enable by default - TRUE - - }) - - # bail if we're not enabled - if (!enabled) { - - # if we're not enabled, we might still need to manually load - # the user profile here - profile <- Sys.getenv("R_PROFILE_USER", unset = "~/.Rprofile") - if (file.exists(profile)) { - cfg <- Sys.getenv("RENV_CONFIG_USER_PROFILE", unset = "TRUE") - if (tolower(cfg) %in% c("true", "t", "1")) - sys.source(profile, envir = globalenv()) - } - - return(FALSE) - - } - - # avoid recursion - if (identical(getOption("renv.autoloader.running"), TRUE)) { - warning("ignoring recursive attempt to run renv autoloader") - return(invisible(TRUE)) - } - - # signal that we're loading renv during R startup - options(renv.autoloader.running = TRUE) - on.exit(options(renv.autoloader.running = NULL), add = TRUE) - - # signal that we've consented to use renv - options(renv.consent = TRUE) - - # load the 'utils' package eagerly -- this ensures that renv shims, which - # mask 'utils' packages, will come first on the search path - library(utils, lib.loc = .Library) - - # unload renv if it's already been loaded - if ("renv" %in% loadedNamespaces()) - unloadNamespace("renv") - - # load bootstrap tools - ansify <- function(text) { - if (renv_ansify_enabled()) - renv_ansify_enhanced(text) - else - renv_ansify_default(text) - } - - renv_ansify_enabled <- function() { - - override <- Sys.getenv("RENV_ANSIFY_ENABLED", unset = NA) - if (!is.na(override)) - return(as.logical(override)) - - pane <- Sys.getenv("RSTUDIO_CHILD_PROCESS_PANE", unset = NA) - if (identical(pane, "build")) - return(FALSE) - - testthat <- Sys.getenv("TESTTHAT", unset = "false") - if (tolower(testthat) %in% "true") - return(FALSE) - - iderun <- Sys.getenv("R_CLI_HAS_HYPERLINK_IDE_RUN", unset = "false") - if (tolower(iderun) %in% "false") - return(FALSE) - - TRUE - - } - - renv_ansify_default <- function(text) { - text - } - - renv_ansify_enhanced <- function(text) { - - # R help links - pattern <- "`\\?(renv::(?:[^`])+)`" - replacement <- "`\033]8;;x-r-help:\\1\a?\\1\033]8;;\a`" - text <- gsub(pattern, replacement, text, perl = TRUE) - - # runnable code - pattern <- "`(renv::(?:[^`])+)`" - replacement <- "`\033]8;;x-r-run:\\1\a\\1\033]8;;\a`" - text <- gsub(pattern, replacement, text, perl = TRUE) - - # return ansified text - text - - } - - renv_ansify_init <- function() { - - envir <- renv_envir_self() - if (renv_ansify_enabled()) - assign("ansify", renv_ansify_enhanced, envir = envir) - else - assign("ansify", renv_ansify_default, envir = envir) - - } - - `%||%` <- function(x, y) { - if (is.null(x)) y else x - } - - catf <- function(fmt, ..., appendLF = TRUE) { - - quiet <- getOption("renv.bootstrap.quiet", default = FALSE) - if (quiet) - return(invisible()) - - msg <- sprintf(fmt, ...) - cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") - - invisible(msg) - - } - - header <- function(label, - ..., - prefix = "#", - suffix = "-", - n = min(getOption("width"), 78)) - { - label <- sprintf(label, ...) - n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) - if (n <= 0) - return(paste(prefix, label)) - - tail <- paste(rep.int(suffix, n), collapse = "") - paste0(prefix, " ", label, " ", tail) - - } - - heredoc <- function(text, leave = 0) { - - # remove leading, trailing whitespace - trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) - - # split into lines - lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] - - # compute common indent - indent <- regexpr("[^[:space:]]", lines) - common <- min(setdiff(indent, -1L)) - leave - text <- paste(substring(lines, common), collapse = "\n") - - # substitute in ANSI links for executable renv code - ansify(text) - - } - - bootstrap <- function(version, library) { - - friendly <- renv_bootstrap_version_friendly(version) - section <- header(sprintf("Bootstrapping renv %s", friendly)) - catf(section) - - # attempt to download renv - catf("- Downloading renv ... ", appendLF = FALSE) - withCallingHandlers( - tarball <- renv_bootstrap_download(version), - error = function(err) { - catf("FAILED") - stop("failed to download:\n", conditionMessage(err)) - } - ) - catf("OK") - on.exit(unlink(tarball), add = TRUE) - - # now attempt to install - catf("- Installing renv ... ", appendLF = FALSE) - withCallingHandlers( - status <- renv_bootstrap_install(version, tarball, library), - error = function(err) { - catf("FAILED") - stop("failed to install:\n", conditionMessage(err)) - } - ) - catf("OK") - - # add empty line to break up bootstrapping from normal output - catf("") - - return(invisible()) - } - - renv_bootstrap_tests_running <- function() { - getOption("renv.tests.running", default = FALSE) - } - - renv_bootstrap_repos <- function() { - - # get CRAN repository - cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") - - # check for repos override - repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) - if (!is.na(repos)) { - - # check for RSPM; if set, use a fallback repository for renv - rspm <- Sys.getenv("RSPM", unset = NA) - if (identical(rspm, repos)) - repos <- c(RSPM = rspm, CRAN = cran) - - return(repos) - - } - - # check for lockfile repositories - repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) - if (!inherits(repos, "error") && length(repos)) - return(repos) - - # retrieve current repos - repos <- getOption("repos") - - # ensure @CRAN@ entries are resolved - repos[repos == "@CRAN@"] <- cran - - # add in renv.bootstrap.repos if set - default <- c(FALLBACK = "https://cloud.r-project.org") - extra <- getOption("renv.bootstrap.repos", default = default) - repos <- c(repos, extra) - - # remove duplicates that might've snuck in - dupes <- duplicated(repos) | duplicated(names(repos)) - repos[!dupes] - - } - - renv_bootstrap_repos_lockfile <- function() { - - lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") - if (!file.exists(lockpath)) - return(NULL) - - lockfile <- tryCatch(renv_json_read(lockpath), error = identity) - if (inherits(lockfile, "error")) { - warning(lockfile) - return(NULL) - } - - repos <- lockfile$R$Repositories - if (length(repos) == 0) - return(NULL) - - keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) - vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) - names(vals) <- keys - - return(vals) - - } - - renv_bootstrap_download <- function(version) { - - sha <- attr(version, "sha", exact = TRUE) - - methods <- if (!is.null(sha)) { - - # attempting to bootstrap a development version of renv - c( - function() renv_bootstrap_download_tarball(sha), - function() renv_bootstrap_download_github(sha) - ) - - } else { - - # attempting to bootstrap a release version of renv - c( - function() renv_bootstrap_download_tarball(version), - function() renv_bootstrap_download_cran_latest(version), - function() renv_bootstrap_download_cran_archive(version) - ) - - } - - for (method in methods) { - path <- tryCatch(method(), error = identity) - if (is.character(path) && file.exists(path)) - return(path) - } - - stop("All download methods failed") - - } - - renv_bootstrap_download_impl <- function(url, destfile) { - - mode <- "wb" - - # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 - fixup <- - Sys.info()[["sysname"]] == "Windows" && - substring(url, 1L, 5L) == "file:" - - if (fixup) - mode <- "w+b" - - args <- list( - url = url, - destfile = destfile, - mode = mode, - quiet = TRUE - ) - - if ("headers" %in% names(formals(utils::download.file))) { - headers <- renv_bootstrap_download_custom_headers(url) - if (length(headers) && is.character(headers)) - args$headers <- headers - } - - do.call(utils::download.file, args) - - } - - renv_bootstrap_download_custom_headers <- function(url) { - - headers <- getOption("renv.download.headers") - if (is.null(headers)) - return(character()) - - if (!is.function(headers)) - stopf("'renv.download.headers' is not a function") - - headers <- headers(url) - if (length(headers) == 0L) - return(character()) - - if (is.list(headers)) - headers <- unlist(headers, recursive = FALSE, use.names = TRUE) - - ok <- - is.character(headers) && - is.character(names(headers)) && - all(nzchar(names(headers))) - - if (!ok) - stop("invocation of 'renv.download.headers' did not return a named character vector") - - headers - - } - - renv_bootstrap_download_cran_latest <- function(version) { - - spec <- renv_bootstrap_download_cran_latest_find(version) - type <- spec$type - repos <- spec$repos - - baseurl <- utils::contrib.url(repos = repos, type = type) - ext <- if (identical(type, "source")) - ".tar.gz" - else if (Sys.info()[["sysname"]] == "Windows") - ".zip" - else - ".tgz" - name <- sprintf("renv_%s%s", version, ext) - url <- paste(baseurl, name, sep = "/") - - destfile <- file.path(tempdir(), name) - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (inherits(status, "condition")) - return(FALSE) - - # report success and return - destfile - - } - - renv_bootstrap_download_cran_latest_find <- function(version) { - - # check whether binaries are supported on this system - binary <- - getOption("renv.bootstrap.binary", default = TRUE) && - !identical(.Platform$pkgType, "source") && - !identical(getOption("pkgType"), "source") && - Sys.info()[["sysname"]] %in% c("Darwin", "Windows") - - types <- c(if (binary) "binary", "source") - - # iterate over types + repositories - for (type in types) { - for (repos in renv_bootstrap_repos()) { - - # build arguments for utils::available.packages() call - args <- list(type = type, repos = repos) - - # add custom headers if available -- note that - # utils::available.packages() will pass this to download.file() - if ("headers" %in% names(formals(utils::download.file))) { - headers <- renv_bootstrap_download_custom_headers(repos) - if (length(headers) && is.character(headers)) - args$headers <- headers - } - - # retrieve package database - db <- tryCatch( - as.data.frame( - do.call(utils::available.packages, args), - stringsAsFactors = FALSE - ), - error = identity - ) - - if (inherits(db, "error")) - next - - # check for compatible entry - entry <- db[db$Package %in% "renv" & db$Version %in% version, ] - if (nrow(entry) == 0) - next - - # found it; return spec to caller - spec <- list(entry = entry, type = type, repos = repos) - return(spec) - - } - } - - # if we got here, we failed to find renv - fmt <- "renv %s is not available from your declared package repositories" - stop(sprintf(fmt, version)) - - } - - renv_bootstrap_download_cran_archive <- function(version) { - - name <- sprintf("renv_%s.tar.gz", version) - repos <- renv_bootstrap_repos() - urls <- file.path(repos, "src/contrib/Archive/renv", name) - destfile <- file.path(tempdir(), name) - - for (url in urls) { - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (identical(status, 0L)) - return(destfile) - - } - - return(FALSE) - - } - - renv_bootstrap_download_tarball <- function(version) { - - # if the user has provided the path to a tarball via - # an environment variable, then use it - tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) - if (is.na(tarball)) - return() - - # allow directories - if (dir.exists(tarball)) { - name <- sprintf("renv_%s.tar.gz", version) - tarball <- file.path(tarball, name) - } - - # bail if it doesn't exist - if (!file.exists(tarball)) { - - # let the user know we weren't able to honour their request - fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." - msg <- sprintf(fmt, tarball) - warning(msg) - - # bail - return() - - } - - catf("- Using local tarball '%s'.", tarball) - tarball - - } - - renv_bootstrap_github_token <- function() { - for (envvar in c("GITHUB_TOKEN", "GITHUB_PAT", "GH_TOKEN")) { - envval <- Sys.getenv(envvar, unset = NA) - if (!is.na(envval)) - return(envval) - } - } - - renv_bootstrap_download_github <- function(version) { - - enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") - if (!identical(enabled, "TRUE")) - return(FALSE) - - # prepare download options - token <- renv_bootstrap_github_token() - if (is.null(token)) - token <- "" - - if (nzchar(Sys.which("curl")) && nzchar(token)) { - fmt <- "--location --fail --header \"Authorization: token %s\"" - extra <- sprintf(fmt, token) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "curl", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } else if (nzchar(Sys.which("wget")) && nzchar(token)) { - fmt <- "--header=\"Authorization: token %s\"" - extra <- sprintf(fmt, token) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "wget", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } - - url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) - name <- sprintf("renv_%s.tar.gz", version) - destfile <- file.path(tempdir(), name) - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (!identical(status, 0L)) - return(FALSE) - - renv_bootstrap_download_augment(destfile) - - return(destfile) - - } - - # Add Sha to DESCRIPTION. This is stop gap until #890, after which we - # can use renv::install() to fully capture metadata. - renv_bootstrap_download_augment <- function(destfile) { - sha <- renv_bootstrap_git_extract_sha1_tar(destfile) - if (is.null(sha)) { - return() - } - - # Untar - tempdir <- tempfile("renv-github-") - on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) - untar(destfile, exdir = tempdir) - pkgdir <- dir(tempdir, full.names = TRUE)[[1]] - - # Modify description - desc_path <- file.path(pkgdir, "DESCRIPTION") - desc_lines <- readLines(desc_path) - remotes_fields <- c( - "RemoteType: github", - "RemoteHost: api.github.com", - "RemoteRepo: renv", - "RemoteUsername: rstudio", - "RemotePkgRef: rstudio/renv", - paste("RemoteRef: ", sha), - paste("RemoteSha: ", sha) - ) - writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) - - # Re-tar - local({ - old <- setwd(tempdir) - on.exit(setwd(old), add = TRUE) - - tar(destfile, compression = "gzip") - }) - invisible() - } - - # Extract the commit hash from a git archive. Git archives include the SHA1 - # hash as the comment field of the tarball pax extended header - # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) - # For GitHub archives this should be the first header after the default one - # (512 byte) header. - renv_bootstrap_git_extract_sha1_tar <- function(bundle) { - - # open the bundle for reading - # We use gzcon for everything because (from ?gzcon) - # > Reading from a connection which does not supply a 'gzip' magic - # > header is equivalent to reading from the original connection - conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) - on.exit(close(conn)) - - # The default pax header is 512 bytes long and the first pax extended header - # with the comment should be 51 bytes long - # `52 comment=` (11 chars) + 40 byte SHA1 hash - len <- 0x200 + 0x33 - res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) - - if (grepl("^52 comment=", res)) { - sub("52 comment=", "", res) - } else { - NULL - } - } - - renv_bootstrap_install <- function(version, tarball, library) { - - # attempt to install it into project library - dir.create(library, showWarnings = FALSE, recursive = TRUE) - output <- renv_bootstrap_install_impl(library, tarball) - - # check for successful install - status <- attr(output, "status") - if (is.null(status) || identical(status, 0L)) - return(status) - - # an error occurred; report it - header <- "installation of renv failed" - lines <- paste(rep.int("=", nchar(header)), collapse = "") - text <- paste(c(header, lines, output), collapse = "\n") - stop(text) - - } - - renv_bootstrap_install_impl <- function(library, tarball) { - - # invoke using system2 so we can capture and report output - bin <- R.home("bin") - exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" - R <- file.path(bin, exe) - - args <- c( - "--vanilla", "CMD", "INSTALL", "--no-multiarch", - "-l", shQuote(path.expand(library)), - shQuote(path.expand(tarball)) - ) - - system2(R, args, stdout = TRUE, stderr = TRUE) - - } - - renv_bootstrap_platform_prefix_default <- function() { - - # read version component - version <- Sys.getenv("RENV_PATHS_VERSION", unset = "R-%v") - - # expand placeholders - placeholders <- list( - list("%v", format(getRversion()[1, 1:2])), - list("%V", format(getRversion()[1, 1:3])) - ) - - for (placeholder in placeholders) - version <- gsub(placeholder[[1L]], placeholder[[2L]], version, fixed = TRUE) - - # include SVN revision for development versions of R - # (to avoid sharing platform-specific artefacts with released versions of R) - devel <- - identical(R.version[["status"]], "Under development (unstable)") || - identical(R.version[["nickname"]], "Unsuffered Consequences") - - if (devel) - version <- paste(version, R.version[["svn rev"]], sep = "-r") - - version - - } - - renv_bootstrap_platform_prefix <- function() { - - # construct version prefix - version <- renv_bootstrap_platform_prefix_default() - - # build list of path components - components <- c(version, R.version$platform) - - # include prefix if provided by user - prefix <- renv_bootstrap_platform_prefix_impl() - if (!is.na(prefix) && nzchar(prefix)) - components <- c(prefix, components) - - # build prefix - paste(components, collapse = "/") - - } - - renv_bootstrap_platform_prefix_impl <- function() { - - # if an explicit prefix has been supplied, use it - prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) - if (!is.na(prefix)) - return(prefix) - - # if the user has requested an automatic prefix, generate it - auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) - if (is.na(auto) && getRversion() >= "4.4.0") - auto <- "TRUE" - - if (auto %in% c("TRUE", "True", "true", "1")) - return(renv_bootstrap_platform_prefix_auto()) - - # empty string on failure - "" - - } - - renv_bootstrap_platform_prefix_auto <- function() { - - prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) - if (inherits(prefix, "error") || prefix %in% "unknown") { - - msg <- paste( - "failed to infer current operating system", - "please file a bug report at https://github.com/rstudio/renv/issues", - sep = "; " - ) - - warning(msg) - - } - - prefix - - } - - renv_bootstrap_platform_os <- function() { - - sysinfo <- Sys.info() - sysname <- sysinfo[["sysname"]] - - # handle Windows + macOS up front - if (sysname == "Windows") - return("windows") - else if (sysname == "Darwin") - return("macos") - - # check for os-release files - for (file in c("/etc/os-release", "/usr/lib/os-release")) - if (file.exists(file)) - return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) - - # check for redhat-release files - if (file.exists("/etc/redhat-release")) - return(renv_bootstrap_platform_os_via_redhat_release()) - - "unknown" - - } - - renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { - - # read /etc/os-release - release <- utils::read.table( - file = file, - sep = "=", - quote = c("\"", "'"), - col.names = c("Key", "Value"), - comment.char = "#", - stringsAsFactors = FALSE - ) - - vars <- as.list(release$Value) - names(vars) <- release$Key - - # get os name - os <- tolower(sysinfo[["sysname"]]) - - # read id - id <- "unknown" - for (field in c("ID", "ID_LIKE")) { - if (field %in% names(vars) && nzchar(vars[[field]])) { - id <- vars[[field]] - break - } - } - - # read version - version <- "unknown" - for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { - if (field %in% names(vars) && nzchar(vars[[field]])) { - version <- vars[[field]] - break - } - } - - # join together - paste(c(os, id, version), collapse = "-") - - } - - renv_bootstrap_platform_os_via_redhat_release <- function() { - - # read /etc/redhat-release - contents <- readLines("/etc/redhat-release", warn = FALSE) - - # infer id - id <- if (grepl("centos", contents, ignore.case = TRUE)) - "centos" - else if (grepl("redhat", contents, ignore.case = TRUE)) - "redhat" - else - "unknown" - - # try to find a version component (very hacky) - version <- "unknown" - - parts <- strsplit(contents, "[[:space:]]")[[1L]] - for (part in parts) { - - nv <- tryCatch(numeric_version(part), error = identity) - if (inherits(nv, "error")) - next - - version <- nv[1, 1] - break - - } - - paste(c("linux", id, version), collapse = "-") - - } - - renv_bootstrap_library_root_name <- function(project) { - - # use project name as-is if requested - asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") - if (asis) - return(basename(project)) - - # otherwise, disambiguate based on project's path - id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) - paste(basename(project), id, sep = "-") - - } - - renv_bootstrap_library_root <- function(project) { - - prefix <- renv_bootstrap_profile_prefix() - - path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) - if (!is.na(path)) - return(paste(c(path, prefix), collapse = "/")) - - path <- renv_bootstrap_library_root_impl(project) - if (!is.null(path)) { - name <- renv_bootstrap_library_root_name(project) - return(paste(c(path, prefix, name), collapse = "/")) - } - - renv_bootstrap_paths_renv("library", project = project) - - } - - renv_bootstrap_library_root_impl <- function(project) { - - root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) - if (!is.na(root)) - return(root) - - type <- renv_bootstrap_project_type(project) - if (identical(type, "package")) { - userdir <- renv_bootstrap_user_dir() - return(file.path(userdir, "library")) - } - - } - - renv_bootstrap_validate_version <- function(version, description = NULL) { - - # resolve description file - # - # avoid passing lib.loc to `packageDescription()` below, since R will - # use the loaded version of the package by default anyhow. note that - # this function should only be called after 'renv' is loaded - # https://github.com/rstudio/renv/issues/1625 - description <- description %||% packageDescription("renv") - - # check whether requested version 'version' matches loaded version of renv - sha <- attr(version, "sha", exact = TRUE) - valid <- if (!is.null(sha)) - renv_bootstrap_validate_version_dev(sha, description) - else - renv_bootstrap_validate_version_release(version, description) - - if (valid) - return(TRUE) - - # the loaded version of renv doesn't match the requested version; - # give the user instructions on how to proceed - dev <- identical(description[["RemoteType"]], "github") - remote <- if (dev) - paste("rstudio/renv", description[["RemoteSha"]], sep = "@") - else - paste("renv", description[["Version"]], sep = "@") - - # display both loaded version + sha if available - friendly <- renv_bootstrap_version_friendly( - version = description[["Version"]], - sha = if (dev) description[["RemoteSha"]] - ) - - fmt <- heredoc(" - renv %1$s was loaded from project library, but this project is configured to use renv %2$s. - - Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile. - - Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library. - ") - catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) - - FALSE - - } - - renv_bootstrap_validate_version_dev <- function(version, description) { - - expected <- description[["RemoteSha"]] - if (!is.character(expected)) - return(FALSE) - - pattern <- sprintf("^\\Q%s\\E", version) - grepl(pattern, expected, perl = TRUE) - - } - - renv_bootstrap_validate_version_release <- function(version, description) { - expected <- description[["Version"]] - is.character(expected) && identical(expected, version) - } - - renv_bootstrap_hash_text <- function(text) { - - hashfile <- tempfile("renv-hash-") - on.exit(unlink(hashfile), add = TRUE) - - writeLines(text, con = hashfile) - tools::md5sum(hashfile) - - } - - renv_bootstrap_load <- function(project, libpath, version) { - - # try to load renv from the project library - if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) - return(FALSE) - - # warn if the version of renv loaded does not match - renv_bootstrap_validate_version(version) - - # execute renv load hooks, if any - hooks <- getHook("renv::autoload") - for (hook in hooks) - if (is.function(hook)) - tryCatch(hook(), error = warnify) - - # load the project - renv::load(project) - - TRUE - - } - - renv_bootstrap_profile_load <- function(project) { - - # if RENV_PROFILE is already set, just use that - profile <- Sys.getenv("RENV_PROFILE", unset = NA) - if (!is.na(profile) && nzchar(profile)) - return(profile) - - # check for a profile file (nothing to do if it doesn't exist) - path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) - if (!file.exists(path)) - return(NULL) - - # read the profile, and set it if it exists - contents <- readLines(path, warn = FALSE) - if (length(contents) == 0L) - return(NULL) - - # set RENV_PROFILE - profile <- contents[[1L]] - if (!profile %in% c("", "default")) - Sys.setenv(RENV_PROFILE = profile) - - profile - - } - - renv_bootstrap_profile_prefix <- function() { - profile <- renv_bootstrap_profile_get() - if (!is.null(profile)) - return(file.path("profiles", profile, "renv")) - } - - renv_bootstrap_profile_get <- function() { - profile <- Sys.getenv("RENV_PROFILE", unset = "") - renv_bootstrap_profile_normalize(profile) - } - - renv_bootstrap_profile_set <- function(profile) { - profile <- renv_bootstrap_profile_normalize(profile) - if (is.null(profile)) - Sys.unsetenv("RENV_PROFILE") - else - Sys.setenv(RENV_PROFILE = profile) - } - - renv_bootstrap_profile_normalize <- function(profile) { - - if (is.null(profile) || profile %in% c("", "default")) - return(NULL) - - profile - - } - - renv_bootstrap_path_absolute <- function(path) { - - substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( - substr(path, 1L, 1L) %in% c(letters, LETTERS) && - substr(path, 2L, 3L) %in% c(":/", ":\\") - ) - - } - - renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { - renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") - root <- if (renv_bootstrap_path_absolute(renv)) NULL else project - prefix <- if (profile) renv_bootstrap_profile_prefix() - components <- c(root, renv, prefix, ...) - paste(components, collapse = "/") - } - - renv_bootstrap_project_type <- function(path) { - - descpath <- file.path(path, "DESCRIPTION") - if (!file.exists(descpath)) - return("unknown") - - desc <- tryCatch( - read.dcf(descpath, all = TRUE), - error = identity - ) - - if (inherits(desc, "error")) - return("unknown") - - type <- desc$Type - if (!is.null(type)) - return(tolower(type)) - - package <- desc$Package - if (!is.null(package)) - return("package") - - "unknown" - - } - - renv_bootstrap_user_dir <- function() { - dir <- renv_bootstrap_user_dir_impl() - path.expand(chartr("\\", "/", dir)) - } - - renv_bootstrap_user_dir_impl <- function() { - - # use local override if set - override <- getOption("renv.userdir.override") - if (!is.null(override)) - return(override) - - # use R_user_dir if available - tools <- asNamespace("tools") - if (is.function(tools$R_user_dir)) - return(tools$R_user_dir("renv", "cache")) - - # try using our own backfill for older versions of R - envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") - for (envvar in envvars) { - root <- Sys.getenv(envvar, unset = NA) - if (!is.na(root)) - return(file.path(root, "R/renv")) - } - - # use platform-specific default fallbacks - if (Sys.info()[["sysname"]] == "Windows") - file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") - else if (Sys.info()[["sysname"]] == "Darwin") - "~/Library/Caches/org.R-project.R/R/renv" - else - "~/.cache/R/renv" - - } - - renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { - sha <- sha %||% attr(version, "sha", exact = TRUE) - parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) - paste(parts, collapse = "") - } - - renv_bootstrap_exec <- function(project, libpath, version) { - if (!renv_bootstrap_load(project, libpath, version)) - renv_bootstrap_run(project, libpath, version) - } - - renv_bootstrap_run <- function(project, libpath, version) { - - # perform bootstrap - bootstrap(version, libpath) - - # exit early if we're just testing bootstrap - if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) - return(TRUE) - - # try again to load - if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { - return(renv::load(project = project)) - } - - # failed to download or load renv; warn the user - msg <- c( - "Failed to find an renv installation: the project will not be loaded.", - "Use `renv::activate()` to re-initialize the project." - ) - - warning(paste(msg, collapse = "\n"), call. = FALSE) - - } - - renv_json_read <- function(file = NULL, text = NULL) { - - jlerr <- NULL - - # if jsonlite is loaded, use that instead - if ("jsonlite" %in% loadedNamespaces()) { - - json <- tryCatch(renv_json_read_jsonlite(file, text), error = identity) - if (!inherits(json, "error")) - return(json) - - jlerr <- json - - } - - # otherwise, fall back to the default JSON reader - json <- tryCatch(renv_json_read_default(file, text), error = identity) - if (!inherits(json, "error")) - return(json) - - # report an error - if (!is.null(jlerr)) - stop(jlerr) - else - stop(json) - - } - - renv_json_read_jsonlite <- function(file = NULL, text = NULL) { - text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") - jsonlite::fromJSON(txt = text, simplifyVector = FALSE) - } - - renv_json_read_patterns <- function() { - - list( - - # objects - list("{", "\t\n\tobject(\t\n\t", TRUE), - list("}", "\t\n\t)\t\n\t", TRUE), - - # arrays - list("[", "\t\n\tarray(\t\n\t", TRUE), - list("]", "\n\t\n)\n\t\n", TRUE), - - # maps - list(":", "\t\n\t=\t\n\t", TRUE), - - # newlines - list("\\u000a", "\n", FALSE) - - ) - - } - - renv_json_read_envir <- function() { - - envir <- new.env(parent = emptyenv()) - - envir[["+"]] <- `+` - envir[["-"]] <- `-` - - envir[["object"]] <- function(...) { - result <- list(...) - names(result) <- as.character(names(result)) - result - } - - envir[["array"]] <- list - - envir[["true"]] <- TRUE - envir[["false"]] <- FALSE - envir[["null"]] <- NULL - - envir - - } - - renv_json_read_remap <- function(object, patterns) { - - # repair names if necessary - if (!is.null(names(object))) { - - nms <- names(object) - for (pattern in patterns) - nms <- gsub(pattern[[2L]], pattern[[1L]], nms, fixed = TRUE) - names(object) <- nms - - } - - # repair strings if necessary - if (is.character(object)) { - for (pattern in patterns) - object <- gsub(pattern[[2L]], pattern[[1L]], object, fixed = TRUE) - } - - # recurse for other objects - if (is.recursive(object)) - for (i in seq_along(object)) - object[i] <- list(renv_json_read_remap(object[[i]], patterns)) - - # return remapped object - object - - } - - renv_json_read_default <- function(file = NULL, text = NULL) { - - # read json text - text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") - - # convert into something the R parser will understand - patterns <- renv_json_read_patterns() - transformed <- text - for (pattern in patterns) - transformed <- gsub(pattern[[1L]], pattern[[2L]], transformed, fixed = TRUE) - - # parse it - rfile <- tempfile("renv-json-", fileext = ".R") - on.exit(unlink(rfile), add = TRUE) - writeLines(transformed, con = rfile) - json <- parse(rfile, keep.source = FALSE, srcfile = NULL)[[1L]] - - # evaluate in safe environment - result <- eval(json, envir = renv_json_read_envir()) - - # fix up strings if necessary -- do so only with reversible patterns - patterns <- Filter(function(pattern) pattern[[3L]], patterns) - renv_json_read_remap(result, patterns) - - } - - - # load the renv profile, if any - renv_bootstrap_profile_load(project) - - # construct path to library root - root <- renv_bootstrap_library_root(project) - - # construct library prefix for platform - prefix <- renv_bootstrap_platform_prefix() - - # construct full libpath - libpath <- file.path(root, prefix) - - # run bootstrap code - renv_bootstrap_exec(project, libpath, version) - - invisible() - -}) + +local({ + + # the requested version of renv + version <- "1.1.4" + attr(version, "sha") <- NULL + + # the project directory + project <- Sys.getenv("RENV_PROJECT") + if (!nzchar(project)) + project <- getwd() + + # use start-up diagnostics if enabled + diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") + if (diagnostics) { + start <- Sys.time() + profile <- tempfile("renv-startup-", fileext = ".Rprof") + utils::Rprof(profile) + on.exit({ + utils::Rprof(NULL) + elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) + writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) + writeLines(sprintf("- Profile: %s", profile)) + print(utils::summaryRprof(profile)) + }, add = TRUE) + } + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # if we're being run in a context where R_LIBS is already set, + # don't load -- presumably we're being run as a sub-process and + # the parent process has already set up library paths for us + rcmd <- Sys.getenv("R_CMD", unset = NA) + rlibs <- Sys.getenv("R_LIBS", unset = NA) + if (!is.na(rlibs) && !is.na(rcmd)) + return(FALSE) + + # next, check environment variables + # prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + # bail if we're not enabled + if (!enabled) { + + # if we're not enabled, we might still need to manually load + # the user profile here + profile <- Sys.getenv("R_PROFILE_USER", unset = "~/.Rprofile") + if (file.exists(profile)) { + cfg <- Sys.getenv("RENV_CONFIG_USER_PROFILE", unset = "TRUE") + if (tolower(cfg) %in% c("true", "t", "1")) + sys.source(profile, envir = globalenv()) + } + + return(FALSE) + + } + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + ansify <- function(text) { + if (renv_ansify_enabled()) + renv_ansify_enhanced(text) + else + renv_ansify_default(text) + } + + renv_ansify_enabled <- function() { + + override <- Sys.getenv("RENV_ANSIFY_ENABLED", unset = NA) + if (!is.na(override)) + return(as.logical(override)) + + pane <- Sys.getenv("RSTUDIO_CHILD_PROCESS_PANE", unset = NA) + if (identical(pane, "build")) + return(FALSE) + + testthat <- Sys.getenv("TESTTHAT", unset = "false") + if (tolower(testthat) %in% "true") + return(FALSE) + + iderun <- Sys.getenv("R_CLI_HAS_HYPERLINK_IDE_RUN", unset = "false") + if (tolower(iderun) %in% "false") + return(FALSE) + + TRUE + + } + + renv_ansify_default <- function(text) { + text + } + + renv_ansify_enhanced <- function(text) { + + # R help links + pattern <- "`\\?(renv::(?:[^`])+)`" + replacement <- "`\033]8;;x-r-help:\\1\a?\\1\033]8;;\a`" + text <- gsub(pattern, replacement, text, perl = TRUE) + + # runnable code + pattern <- "`(renv::(?:[^`])+)`" + replacement <- "`\033]8;;x-r-run:\\1\a\\1\033]8;;\a`" + text <- gsub(pattern, replacement, text, perl = TRUE) + + # return ansified text + text + + } + + renv_ansify_init <- function() { + + envir <- renv_envir_self() + if (renv_ansify_enabled()) + assign("ansify", renv_ansify_enhanced, envir = envir) + else + assign("ansify", renv_ansify_default, envir = envir) + + } + + `%||%` <- function(x, y) { + if (is.null(x)) y else x + } + + catf <- function(fmt, ..., appendLF = TRUE) { + + quiet <- getOption("renv.bootstrap.quiet", default = FALSE) + if (quiet) + return(invisible()) + + msg <- sprintf(fmt, ...) + cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") + + invisible(msg) + + } + + header <- function(label, + ..., + prefix = "#", + suffix = "-", + n = min(getOption("width"), 78)) + { + label <- sprintf(label, ...) + n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) + if (n <= 0) + return(paste(prefix, label)) + + tail <- paste(rep.int(suffix, n), collapse = "") + paste0(prefix, " ", label, " ", tail) + + } + + heredoc <- function(text, leave = 0) { + + # remove leading, trailing whitespace + trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) + + # split into lines + lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] + + # compute common indent + indent <- regexpr("[^[:space:]]", lines) + common <- min(setdiff(indent, -1L)) - leave + text <- paste(substring(lines, common), collapse = "\n") + + # substitute in ANSI links for executable renv code + ansify(text) + + } + + bootstrap <- function(version, library) { + + friendly <- renv_bootstrap_version_friendly(version) + section <- header(sprintf("Bootstrapping renv %s", friendly)) + catf(section) + + # attempt to download renv + catf("- Downloading renv ... ", appendLF = FALSE) + withCallingHandlers( + tarball <- renv_bootstrap_download(version), + error = function(err) { + catf("FAILED") + stop("failed to download:\n", conditionMessage(err)) + } + ) + catf("OK") + on.exit(unlink(tarball), add = TRUE) + + # now attempt to install + catf("- Installing renv ... ", appendLF = FALSE) + withCallingHandlers( + status <- renv_bootstrap_install(version, tarball, library), + error = function(err) { + catf("FAILED") + stop("failed to install:\n", conditionMessage(err)) + } + ) + catf("OK") + + # add empty line to break up bootstrapping from normal output + catf("") + + return(invisible()) + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # check for RSPM; if set, use a fallback repository for renv + rspm <- Sys.getenv("RSPM", unset = NA) + if (identical(rspm, repos)) + repos <- c(RSPM = rspm, CRAN = cran) + + return(repos) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + sha <- attr(version, "sha", exact = TRUE) + + methods <- if (!is.null(sha)) { + + # attempting to bootstrap a development version of renv + c( + function() renv_bootstrap_download_tarball(sha), + function() renv_bootstrap_download_github(sha) + ) + + } else { + + # attempting to bootstrap a release version of renv + c( + function() renv_bootstrap_download_tarball(version), + function() renv_bootstrap_download_cran_latest(version), + function() renv_bootstrap_download_cran_archive(version) + ) + + } + + for (method in methods) { + path <- tryCatch(method(), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("All download methods failed") + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) { + headers <- renv_bootstrap_download_custom_headers(url) + if (length(headers) && is.character(headers)) + args$headers <- headers + } + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) + return(FALSE) + + # report success and return + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # build arguments for utils::available.packages() call + args <- list(type = type, repos = repos) + + # add custom headers if available -- note that + # utils::available.packages() will pass this to download.file() + if ("headers" %in% names(formals(utils::download.file))) { + headers <- renv_bootstrap_download_custom_headers(repos) + if (length(headers) && is.character(headers)) + args$headers <- headers + } + + # retrieve package database + db <- tryCatch( + as.data.frame( + do.call(utils::available.packages, args), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) + return(destfile) + + } + + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + catf("- Using local tarball '%s'.", tarball) + tarball + + } + + renv_bootstrap_github_token <- function() { + for (envvar in c("GITHUB_TOKEN", "GITHUB_PAT", "GH_TOKEN")) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(envval) + } + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + token <- renv_bootstrap_github_token() + if (is.null(token)) + token <- "" + + if (nzchar(Sys.which("curl")) && nzchar(token)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, token) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(token)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, token) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) + return(FALSE) + + renv_bootstrap_download_augment(destfile) + + return(destfile) + + } + + # Add Sha to DESCRIPTION. This is stop gap until #890, after which we + # can use renv::install() to fully capture metadata. + renv_bootstrap_download_augment <- function(destfile) { + sha <- renv_bootstrap_git_extract_sha1_tar(destfile) + if (is.null(sha)) { + return() + } + + # Untar + tempdir <- tempfile("renv-github-") + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + untar(destfile, exdir = tempdir) + pkgdir <- dir(tempdir, full.names = TRUE)[[1]] + + # Modify description + desc_path <- file.path(pkgdir, "DESCRIPTION") + desc_lines <- readLines(desc_path) + remotes_fields <- c( + "RemoteType: github", + "RemoteHost: api.github.com", + "RemoteRepo: renv", + "RemoteUsername: rstudio", + "RemotePkgRef: rstudio/renv", + paste("RemoteRef: ", sha), + paste("RemoteSha: ", sha) + ) + writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) + + # Re-tar + local({ + old <- setwd(tempdir) + on.exit(setwd(old), add = TRUE) + + tar(destfile, compression = "gzip") + }) + invisible() + } + + # Extract the commit hash from a git archive. Git archives include the SHA1 + # hash as the comment field of the tarball pax extended header + # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) + # For GitHub archives this should be the first header after the default one + # (512 byte) header. + renv_bootstrap_git_extract_sha1_tar <- function(bundle) { + + # open the bundle for reading + # We use gzcon for everything because (from ?gzcon) + # > Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix_default <- function() { + + # read version component + version <- Sys.getenv("RENV_PATHS_VERSION", unset = "R-%v") + + # expand placeholders + placeholders <- list( + list("%v", format(getRversion()[1, 1:2])), + list("%V", format(getRversion()[1, 1:3])) + ) + + for (placeholder in placeholders) + version <- gsub(placeholder[[1L]], placeholder[[2L]], version, fixed = TRUE) + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + version <- paste(version, R.version[["svn rev"]], sep = "-r") + + version + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- renv_bootstrap_platform_prefix_default() + + # build list of path components + components <- c(version, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (is.na(auto) && getRversion() >= "4.4.0") + auto <- "TRUE" + + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + dev <- identical(description[["RemoteType"]], "github") + remote <- if (dev) + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + else + paste("renv", description[["Version"]], sep = "@") + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = if (dev) description[["RemoteSha"]] + ) + + fmt <- heredoc(" + renv %1$s was loaded from project library, but this project is configured to use renv %2$s. + - Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile. + - Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library. + ") + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + + expected <- description[["RemoteSha"]] + if (!is.character(expected)) + return(FALSE) + + pattern <- sprintf("^\\Q%s\\E", version) + grepl(pattern, expected, perl = TRUE) + + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(expected, version) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(project, libpath, version) + } + + renv_bootstrap_run <- function(project, libpath, version) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = project)) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- tryCatch(renv_json_read_jsonlite(file, text), error = identity) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- tryCatch(renv_json_read_default(file, text), error = identity) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_patterns <- function() { + + list( + + # objects + list("{", "\t\n\tobject(\t\n\t", TRUE), + list("}", "\t\n\t)\t\n\t", TRUE), + + # arrays + list("[", "\t\n\tarray(\t\n\t", TRUE), + list("]", "\n\t\n)\n\t\n", TRUE), + + # maps + list(":", "\t\n\t=\t\n\t", TRUE), + + # newlines + list("\\u000a", "\n", FALSE) + + ) + + } + + renv_json_read_envir <- function() { + + envir <- new.env(parent = emptyenv()) + + envir[["+"]] <- `+` + envir[["-"]] <- `-` + + envir[["object"]] <- function(...) { + result <- list(...) + names(result) <- as.character(names(result)) + result + } + + envir[["array"]] <- list + + envir[["true"]] <- TRUE + envir[["false"]] <- FALSE + envir[["null"]] <- NULL + + envir + + } + + renv_json_read_remap <- function(object, patterns) { + + # repair names if necessary + if (!is.null(names(object))) { + + nms <- names(object) + for (pattern in patterns) + nms <- gsub(pattern[[2L]], pattern[[1L]], nms, fixed = TRUE) + names(object) <- nms + + } + + # repair strings if necessary + if (is.character(object)) { + for (pattern in patterns) + object <- gsub(pattern[[2L]], pattern[[1L]], object, fixed = TRUE) + } + + # recurse for other objects + if (is.recursive(object)) + for (i in seq_along(object)) + object[i] <- list(renv_json_read_remap(object[[i]], patterns)) + + # return remapped object + object + + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # read json text + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") + + # convert into something the R parser will understand + patterns <- renv_json_read_patterns() + transformed <- text + for (pattern in patterns) + transformed <- gsub(pattern[[1L]], pattern[[2L]], transformed, fixed = TRUE) + + # parse it + rfile <- tempfile("renv-json-", fileext = ".R") + on.exit(unlink(rfile), add = TRUE) + writeLines(transformed, con = rfile) + json <- parse(rfile, keep.source = FALSE, srcfile = NULL)[[1L]] + + # evaluate in safe environment + result <- eval(json, envir = renv_json_read_envir()) + + # fix up strings if necessary -- do so only with reversible patterns + patterns <- Filter(function(pattern) pattern[[3L]], patterns) + renv_json_read_remap(result, patterns) + + } + + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index 43f3390..5fab9b5 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -1,360 +1,360 @@ -# Test suite for parsing functionality -# Tests for parse_put_annotation(), parse_comma_separated_pairs(), and related functions - -library(testthat) - -# Test basic PUT annotation parsing -test_that("parse_put_annotation() handles basic formats correctly", { - # Standard format - result1 <- parse_put_annotation('#put id:"test", label:"Test Label"') - expect_equal(result1$id, "test") - expect_equal(result1$label, "Test Label") - expect_equal(length(result1), 2) - - # With spaces after # - result2 <- parse_put_annotation('# put id:"test2", node_type:"process"') - expect_equal(result2$id, "test2") - expect_equal(result2$node_type, "process") - - # With pipe separator - result3 <- parse_put_annotation('#put| id:"test3", input:"data.csv"') - expect_equal(result3$id, "test3") - expect_equal(result3$input, "data.csv") - - # With colon separator - result4 <- parse_put_annotation('#put: id:"test4", output:"result.txt"') - expect_equal(result4$id, "test4") - expect_equal(result4$output, "result.txt") -}) - -test_that("parse_put_annotation() handles different quote styles", { - # Single quotes - result1 <- parse_put_annotation("#put id:'test1', label:'Single Quotes'") - expect_equal(result1$id, "test1") - expect_equal(result1$label, "Single Quotes") - - # Mixed quotes - result2 <- parse_put_annotation('#put id:"test2", label:\'Mixed Quotes\'') - expect_equal(result2$id, "test2") - expect_equal(result2$label, "Mixed Quotes") - - # Double quotes (standard) - result3 <- parse_put_annotation('#put id:"test3", label:"Double Quotes"') - expect_equal(result3$id, "test3") - expect_equal(result3$label, "Double Quotes") -}) - -test_that("parse_put_annotation() handles whitespace correctly", { - # Extra spaces around properties - result1 <- parse_put_annotation('#put id : "test" , label : "Test Label" ') - expect_equal(result1$id, "test") - expect_equal(result1$label, "Test Label") - - # Tabs and mixed whitespace - result2 <- parse_put_annotation("#put\tid:\t\"test\",\t\tlabel:\t\"Test\"") - expect_equal(result2$id, "test") - expect_equal(result2$label, "Test") - - # Leading and trailing whitespace in values should be preserved - result3 <- parse_put_annotation('#put id:" test ", label:" Test Label "') - expect_equal(result3$id, " test ") # Preserve internal whitespace - expect_equal(result3$label, " Test Label ") -}) - -test_that("parse_put_annotation() handles empty and invalid inputs", { - # Empty PUT annotation - expect_null(parse_put_annotation("#put")) - expect_null(parse_put_annotation("# put ")) - expect_null(parse_put_annotation("#put|")) - expect_null(parse_put_annotation("#put:")) - - # Only whitespace after PUT - expect_null(parse_put_annotation("#put ")) - expect_null(parse_put_annotation("# put \t\n")) - - # Invalid syntax - no quotes - expect_null(parse_put_annotation("#put id:test")) - expect_null(parse_put_annotation("#put id:test, label:label")) - - # Invalid syntax - malformed - expect_null(parse_put_annotation("#put invalid")) - expect_null(parse_put_annotation("#put id")) - expect_null(parse_put_annotation("#put :")) - - # Not a PUT annotation - expect_null(parse_put_annotation("# Regular comment")) - expect_null(parse_put_annotation("puts name:test")) - expect_null(parse_put_annotation("put name:test")) -}) - -test_that("parse_put_annotation() handles complex property values", { - # Commas inside quotes should be preserved - result1 <- parse_put_annotation('#put id:"test", label:"Label with, commas, inside"') - expect_equal(result1$id, "test") - expect_equal(result1$label, "Label with, commas, inside") - - # Multiple commas and complex descriptions - result2 <- parse_put_annotation('#put id:"complex", description:"Process A, B, and C data", files:"file1.csv,file2.csv"') - expect_equal(result2$description, "Process A, B, and C data") - expect_equal(result2$files, "file1.csv,file2.csv") - - # Special characters in values - result3 <- parse_put_annotation('#put name:"special", path:"/path/to/file.csv", regex:"\\\\d+\\.csv$"') - expect_equal(result3$path, "/path/to/file.csv") - expect_equal(result3$regex, "\\\\d+\\.csv$") - - # URLs and complex strings - result4 <- parse_put_annotation('#put name:"url_test", endpoint:"https://api.example.com/data?param=value&other=123"') - expect_equal(result4$endpoint, "https://api.example.com/data?param=value&other=123") -}) - -test_that("parse_put_annotation() handles empty values", { - # Empty string values - result1 <- parse_put_annotation('#put name:"", label:"Not Empty"') - expect_equal(result1$name, "") - expect_equal(result1$label, "Not Empty") - - # Multiple empty values - result2 <- parse_put_annotation('#put name:"test", input:"", output:"", label:"Test"') - expect_equal(result2$name, "test") - expect_equal(result2$input, "") - expect_equal(result2$output, "") - expect_equal(result2$label, "Test") -}) - -test_that("parse_put_annotation() auto-generates UUID when id is missing", { - skip_if_not_installed("uuid", minimum_version = NULL) - - # Missing id should get auto-generated UUID - result1 <- parse_put_annotation('#put label:"Test Node", node_type:"process"') - expect_false(is.null(result1$id)) - expect_true(nchar(result1$id) > 0) - # Check it looks like a UUID (basic check) - expect_true(grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", result1$id, ignore.case = TRUE)) - - # Empty id should NOT get auto-generated - result2 <- parse_put_annotation('#put id:"", label:"Test Node"') - expect_equal(result2$id, "") - - # Explicit id should be preserved - result3 <- parse_put_annotation('#put id:"my_custom_id", label:"Test Node"') - expect_equal(result3$id, "my_custom_id") -}) - -test_that("parse_put_annotation() preserves property order", { - # Properties should be returned in the order they appear - result <- parse_put_annotation('#put zebra:"z", alpha:"a", name:"test", beta:"b", id:"explicit_id"') - - # Check that all properties are present - expect_equal(result$zebra, "z") - expect_equal(result$alpha, "a") - expect_equal(result$name, "test") - expect_equal(result$beta, "b") - expect_equal(result$id, "explicit_id") - expect_equal(length(result), 5) -}) - -# Test the comma-separated parsing function -test_that("parse_comma_separated_pairs() handles basic cases", { - # Basic case - result1 <- parse_comma_separated_pairs('name:"test", label:"Test Label"') - expect_equal(length(result1), 2) - expect_true(any(grepl('name:"test"', result1))) - expect_true(any(grepl('label:"Test Label"', result1))) - - # Single pair - result2 <- parse_comma_separated_pairs('name:"single"') - expect_equal(length(result2), 1) - expect_equal(trimws(result2[1]), 'name:"single"') - - # No pairs (empty string) - result3 <- parse_comma_separated_pairs("") - expect_equal(length(result3), 0) -}) - -test_that("parse_comma_separated_pairs() respects quotes", { - # Commas inside quotes should not split - result1 <- parse_comma_separated_pairs('name:"test", desc:"A, B, and C", type:"complex"') - expect_equal(length(result1), 3) - expect_true(any(grepl("A, B, and C", result1))) - - # Mixed quote types - result2 <- parse_comma_separated_pairs('name:"test", label:\'Single quotes\', desc:"Double quotes"') - expect_equal(length(result2), 3) - expect_true(any(grepl("Single quotes", result2))) - expect_true(any(grepl("Double quotes", result2))) - - # Nested-like scenarios (but no actual nesting) - result3 <- parse_comma_separated_pairs('code:"if (x > 0) { print(\\"positive\\") }", desc:"Complex code"') - expect_equal(length(result3), 2) - expect_true(any(grepl("positive", result3))) -}) - -test_that("parse_comma_separated_pairs() handles edge cases", { - # Extra commas and whitespace - result1 <- parse_comma_separated_pairs('name:"test",,, label:"Test", ') - expect_equal(length(result1), 2) # Empty parts should be filtered out - - # Only commas - result2 <- parse_comma_separated_pairs(",,,") - expect_equal(length(result2), 0) - - # Unmatched quotes (should still try to parse) - result3 <- parse_comma_separated_pairs('name:"test", broken:"unmatched') - expect_equal(length(result3), 2) - expect_true(any(grepl("test", result3))) - expect_true(any(grepl("unmatched", result3))) -}) - -# Test validation functionality -test_that("validate_annotation() handles id validation correctly", { - # Missing id property should NOT generate issues (auto-generated) - props1 <- list(label = "Test", node_type = "process") - issues1 <- validate_annotation(props1, "test line") - expect_false(any(grepl("id", issues1))) - - # Empty id property should generate issue - props2 <- list(id = "", label = "Test", node_type = "process") - issues2 <- validate_annotation(props2, "test line") - expect_true(any(grepl("Empty.*id", issues2))) - - # Valid id property - props3 <- list(id = "valid_id", label = "Test") - issues3 <- validate_annotation(props3, "test line") - expect_false(any(grepl("id", issues3))) -}) - -test_that("validate_annotation() checks node_type values", { - # Invalid node type - props1 <- list(name = "test", node_type = "invalid_type") - issues1 <- validate_annotation(props1, "test line") - expect_true(any(grepl("Unusual node_type", issues1))) - - # Valid node types - valid_types <- c("input", "process", "output", "decision", "start", "end") - for (type in valid_types) { - props <- list(name = "test", node_type = type) - issues <- validate_annotation(props, "test line") - expect_false(any(grepl("Unusual node_type", issues)), - info = paste("Type", type, "should be valid") - ) - } - - # Missing node_type (should not generate warning) - props2 <- list(name = "test", label = "Test") - issues2 <- validate_annotation(props2, "test line") - expect_false(any(grepl("node_type", issues2))) -}) - -test_that("validate_annotation() checks file extensions", { - # File without extension - props1 <- list(name = "test", input = "noextension") - issues1 <- validate_annotation(props1, "test line") - expect_true(any(grepl("missing extension", issues1))) - - # Multiple files, some without extensions - props2 <- list(name = "test", input = "file1.csv", output = "noextension") - issues2 <- validate_annotation(props2, "test line") - expect_true(any(grepl("missing extension", issues2))) - - # All files with extensions (should pass) - props3 <- list(name = "test", input = "input.csv", output = "output.json") - issues3 <- validate_annotation(props3, "test line") - expect_false(any(grepl("missing extension", issues3))) - - # No file references (should pass) - props4 <- list(name = "test", label = "Test") - issues4 <- validate_annotation(props4, "test line") - expect_false(any(grepl("missing extension", issues4))) -}) - -test_that("validate_annotation() handles multiple issues", { - # Multiple validation problems - props <- list( - # id missing (but that's ok - auto-generated) - id = "", # empty id - this is an issue - node_type = "invalid_type", # invalid node type - input = "noextension" # missing file extension - ) - - issues <- validate_annotation(props, "test line") - - # Should catch the three issues (empty id, invalid node_type, missing extension) - expect_true(any(grepl("Empty.*id", issues))) - expect_true(any(grepl("Unusual node_type", issues))) - expect_true(any(grepl("missing extension", issues))) - expect_equal(length(issues), 3) -}) - -# Test the exported validation function -test_that("is_valid_put_annotation() correctly identifies valid annotations", { - # Valid annotations - expect_true(is_valid_put_annotation('#put name:"test", label:"Test"')) - expect_true(is_valid_put_annotation('# put name:"test"')) - expect_true(is_valid_put_annotation('#put| name:"test", type:"process"')) - expect_true(is_valid_put_annotation("#put name:'test', label:'Test'")) - - # Valid with complex values - expect_true(is_valid_put_annotation('#put name:"test", desc:"Complex, description", path:"/path/to/file"')) - - # Invalid annotations - expect_false(is_valid_put_annotation("#put")) - expect_false(is_valid_put_annotation("#put invalid")) - expect_false(is_valid_put_annotation("#put name:noQuotes")) - expect_false(is_valid_put_annotation("not a put annotation")) - expect_false(is_valid_put_annotation("")) - expect_false(is_valid_put_annotation("#comment")) -}) - -# Test error handling in parsing -test_that("parse_put_annotation() handles malformed input gracefully", { - # Should not throw errors, just return NULL - expect_silent(expect_null(parse_put_annotation(NULL))) - expect_silent(expect_null(parse_put_annotation(""))) - expect_silent(expect_null(parse_put_annotation("malformed"))) - - # Very long strings should still work - long_value <- paste(rep("a", 1000), collapse = "") - long_annotation <- paste0('#put name:"test", long_value:"', long_value, '"') - result <- parse_put_annotation(long_annotation) - expect_equal(result$name, "test") - expect_equal(nchar(result$long_value), 1000) -}) - -# Test consistency between parsing and validation -test_that("Parsing and validation work together correctly", { - # Valid annotation should parse and validate - valid_annotation <- '#put id:"test_node", label:"Test Node", node_type:"process"' - parsed <- parse_put_annotation(valid_annotation) - expect_false(is.null(parsed)) - expect_true(is_valid_put_annotation(valid_annotation)) - - issues <- validate_annotation(parsed, valid_annotation) - expect_equal(length(issues), 0) - - # Invalid annotation should not parse - invalid_annotation <- "#put invalid syntax" - parsed_invalid <- parse_put_annotation(invalid_annotation) - expect_null(parsed_invalid) - expect_false(is_valid_put_annotation(invalid_annotation)) -}) - -# Performance tests for parsing -test_that("Parsing performs well with many properties", { - # Create annotation with many properties (including id to prevent auto-generation) - many_props <- paste(paste0("prop", 1:50, ':"value', 1:50, '"'), collapse = ", ") - annotation <- paste0("#put id:\"test\", ", many_props) - - # Should parse without issues - start_time <- Sys.time() - result <- parse_put_annotation(annotation) - end_time <- Sys.time() - - expect_equal(length(result), 51) # 50 props + id - expect_lt(as.numeric(end_time - start_time), 1) # Should take less than 1 second - - # Verify some properties - expect_equal(result$id, "test") - expect_equal(result$prop1, "value1") - expect_equal(result$prop50, "value50") -}) +# Test suite for parsing functionality +# Tests for parse_put_annotation(), parse_comma_separated_pairs(), and related functions + +library(testthat) + +# Test basic PUT annotation parsing +test_that("parse_put_annotation() handles basic formats correctly", { + # Standard format + result1 <- parse_put_annotation('#put id:"test", label:"Test Label"') + expect_equal(result1$id, "test") + expect_equal(result1$label, "Test Label") + expect_equal(length(result1), 2) + + # With spaces after # + result2 <- parse_put_annotation('# put id:"test2", node_type:"process"') + expect_equal(result2$id, "test2") + expect_equal(result2$node_type, "process") + + # With pipe separator + result3 <- parse_put_annotation('#put| id:"test3", input:"data.csv"') + expect_equal(result3$id, "test3") + expect_equal(result3$input, "data.csv") + + # With colon separator + result4 <- parse_put_annotation('#put: id:"test4", output:"result.txt"') + expect_equal(result4$id, "test4") + expect_equal(result4$output, "result.txt") +}) + +test_that("parse_put_annotation() handles different quote styles", { + # Single quotes + result1 <- parse_put_annotation("#put id:'test1', label:'Single Quotes'") + expect_equal(result1$id, "test1") + expect_equal(result1$label, "Single Quotes") + + # Mixed quotes + result2 <- parse_put_annotation('#put id:"test2", label:\'Mixed Quotes\'') + expect_equal(result2$id, "test2") + expect_equal(result2$label, "Mixed Quotes") + + # Double quotes (standard) + result3 <- parse_put_annotation('#put id:"test3", label:"Double Quotes"') + expect_equal(result3$id, "test3") + expect_equal(result3$label, "Double Quotes") +}) + +test_that("parse_put_annotation() handles whitespace correctly", { + # Extra spaces around properties + result1 <- parse_put_annotation('#put id : "test" , label : "Test Label" ') + expect_equal(result1$id, "test") + expect_equal(result1$label, "Test Label") + + # Tabs and mixed whitespace + result2 <- parse_put_annotation("#put\tid:\t\"test\",\t\tlabel:\t\"Test\"") + expect_equal(result2$id, "test") + expect_equal(result2$label, "Test") + + # Leading and trailing whitespace in values should be preserved + result3 <- parse_put_annotation('#put id:" test ", label:" Test Label "') + expect_equal(result3$id, " test ") # Preserve internal whitespace + expect_equal(result3$label, " Test Label ") +}) + +test_that("parse_put_annotation() handles empty and invalid inputs", { + # Empty PUT annotation + expect_null(parse_put_annotation("#put")) + expect_null(parse_put_annotation("# put ")) + expect_null(parse_put_annotation("#put|")) + expect_null(parse_put_annotation("#put:")) + + # Only whitespace after PUT + expect_null(parse_put_annotation("#put ")) + expect_null(parse_put_annotation("# put \t\n")) + + # Invalid syntax - no quotes + expect_null(parse_put_annotation("#put id:test")) + expect_null(parse_put_annotation("#put id:test, label:label")) + + # Invalid syntax - malformed + expect_null(parse_put_annotation("#put invalid")) + expect_null(parse_put_annotation("#put id")) + expect_null(parse_put_annotation("#put :")) + + # Not a PUT annotation + expect_null(parse_put_annotation("# Regular comment")) + expect_null(parse_put_annotation("puts name:test")) + expect_null(parse_put_annotation("put name:test")) +}) + +test_that("parse_put_annotation() handles complex property values", { + # Commas inside quotes should be preserved + result1 <- parse_put_annotation('#put id:"test", label:"Label with, commas, inside"') + expect_equal(result1$id, "test") + expect_equal(result1$label, "Label with, commas, inside") + + # Multiple commas and complex descriptions + result2 <- parse_put_annotation('#put id:"complex", description:"Process A, B, and C data", files:"file1.csv,file2.csv"') + expect_equal(result2$description, "Process A, B, and C data") + expect_equal(result2$files, "file1.csv,file2.csv") + + # Special characters in values + result3 <- parse_put_annotation('#put name:"special", path:"/path/to/file.csv", regex:"\\\\d+\\.csv$"') + expect_equal(result3$path, "/path/to/file.csv") + expect_equal(result3$regex, "\\\\d+\\.csv$") + + # URLs and complex strings + result4 <- parse_put_annotation('#put name:"url_test", endpoint:"https://api.example.com/data?param=value&other=123"') + expect_equal(result4$endpoint, "https://api.example.com/data?param=value&other=123") +}) + +test_that("parse_put_annotation() handles empty values", { + # Empty string values + result1 <- parse_put_annotation('#put name:"", label:"Not Empty"') + expect_equal(result1$name, "") + expect_equal(result1$label, "Not Empty") + + # Multiple empty values + result2 <- parse_put_annotation('#put name:"test", input:"", output:"", label:"Test"') + expect_equal(result2$name, "test") + expect_equal(result2$input, "") + expect_equal(result2$output, "") + expect_equal(result2$label, "Test") +}) + +test_that("parse_put_annotation() auto-generates UUID when id is missing", { + skip_if_not_installed("uuid", minimum_version = NULL) + + # Missing id should get auto-generated UUID + result1 <- parse_put_annotation('#put label:"Test Node", node_type:"process"') + expect_false(is.null(result1$id)) + expect_true(nchar(result1$id) > 0) + # Check it looks like a UUID (basic check) + expect_true(grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", result1$id, ignore.case = TRUE)) + + # Empty id should NOT get auto-generated + result2 <- parse_put_annotation('#put id:"", label:"Test Node"') + expect_equal(result2$id, "") + + # Explicit id should be preserved + result3 <- parse_put_annotation('#put id:"my_custom_id", label:"Test Node"') + expect_equal(result3$id, "my_custom_id") +}) + +test_that("parse_put_annotation() preserves property order", { + # Properties should be returned in the order they appear + result <- parse_put_annotation('#put zebra:"z", alpha:"a", name:"test", beta:"b", id:"explicit_id"') + + # Check that all properties are present + expect_equal(result$zebra, "z") + expect_equal(result$alpha, "a") + expect_equal(result$name, "test") + expect_equal(result$beta, "b") + expect_equal(result$id, "explicit_id") + expect_equal(length(result), 5) +}) + +# Test the comma-separated parsing function +test_that("parse_comma_separated_pairs() handles basic cases", { + # Basic case + result1 <- parse_comma_separated_pairs('name:"test", label:"Test Label"') + expect_equal(length(result1), 2) + expect_true(any(grepl('name:"test"', result1))) + expect_true(any(grepl('label:"Test Label"', result1))) + + # Single pair + result2 <- parse_comma_separated_pairs('name:"single"') + expect_equal(length(result2), 1) + expect_equal(trimws(result2[1]), 'name:"single"') + + # No pairs (empty string) + result3 <- parse_comma_separated_pairs("") + expect_equal(length(result3), 0) +}) + +test_that("parse_comma_separated_pairs() respects quotes", { + # Commas inside quotes should not split + result1 <- parse_comma_separated_pairs('name:"test", desc:"A, B, and C", type:"complex"') + expect_equal(length(result1), 3) + expect_true(any(grepl("A, B, and C", result1))) + + # Mixed quote types + result2 <- parse_comma_separated_pairs('name:"test", label:\'Single quotes\', desc:"Double quotes"') + expect_equal(length(result2), 3) + expect_true(any(grepl("Single quotes", result2))) + expect_true(any(grepl("Double quotes", result2))) + + # Nested-like scenarios (but no actual nesting) + result3 <- parse_comma_separated_pairs('code:"if (x > 0) { print(\\"positive\\") }", desc:"Complex code"') + expect_equal(length(result3), 2) + expect_true(any(grepl("positive", result3))) +}) + +test_that("parse_comma_separated_pairs() handles edge cases", { + # Extra commas and whitespace + result1 <- parse_comma_separated_pairs('name:"test",,, label:"Test", ') + expect_equal(length(result1), 2) # Empty parts should be filtered out + + # Only commas + result2 <- parse_comma_separated_pairs(",,,") + expect_equal(length(result2), 0) + + # Unmatched quotes (should still try to parse) + result3 <- parse_comma_separated_pairs('name:"test", broken:"unmatched') + expect_equal(length(result3), 2) + expect_true(any(grepl("test", result3))) + expect_true(any(grepl("unmatched", result3))) +}) + +# Test validation functionality +test_that("validate_annotation() handles id validation correctly", { + # Missing id property should NOT generate issues (auto-generated) + props1 <- list(label = "Test", node_type = "process") + issues1 <- validate_annotation(props1, "test line") + expect_false(any(grepl("id", issues1))) + + # Empty id property should generate issue + props2 <- list(id = "", label = "Test", node_type = "process") + issues2 <- validate_annotation(props2, "test line") + expect_true(any(grepl("Empty.*id", issues2))) + + # Valid id property + props3 <- list(id = "valid_id", label = "Test") + issues3 <- validate_annotation(props3, "test line") + expect_false(any(grepl("id", issues3))) +}) + +test_that("validate_annotation() checks node_type values", { + # Invalid node type + props1 <- list(name = "test", node_type = "invalid_type") + issues1 <- validate_annotation(props1, "test line") + expect_true(any(grepl("Unusual node_type", issues1))) + + # Valid node types + valid_types <- c("input", "process", "output", "decision", "start", "end") + for (type in valid_types) { + props <- list(name = "test", node_type = type) + issues <- validate_annotation(props, "test line") + expect_false(any(grepl("Unusual node_type", issues)), + info = paste("Type", type, "should be valid") + ) + } + + # Missing node_type (should not generate warning) + props2 <- list(name = "test", label = "Test") + issues2 <- validate_annotation(props2, "test line") + expect_false(any(grepl("node_type", issues2))) +}) + +test_that("validate_annotation() checks file extensions", { + # File without extension + props1 <- list(name = "test", input = "noextension") + issues1 <- validate_annotation(props1, "test line") + expect_true(any(grepl("missing extension", issues1))) + + # Multiple files, some without extensions + props2 <- list(name = "test", input = "file1.csv", output = "noextension") + issues2 <- validate_annotation(props2, "test line") + expect_true(any(grepl("missing extension", issues2))) + + # All files with extensions (should pass) + props3 <- list(name = "test", input = "input.csv", output = "output.json") + issues3 <- validate_annotation(props3, "test line") + expect_false(any(grepl("missing extension", issues3))) + + # No file references (should pass) + props4 <- list(name = "test", label = "Test") + issues4 <- validate_annotation(props4, "test line") + expect_false(any(grepl("missing extension", issues4))) +}) + +test_that("validate_annotation() handles multiple issues", { + # Multiple validation problems + props <- list( + # id missing (but that's ok - auto-generated) + id = "", # empty id - this is an issue + node_type = "invalid_type", # invalid node type + input = "noextension" # missing file extension + ) + + issues <- validate_annotation(props, "test line") + + # Should catch the three issues (empty id, invalid node_type, missing extension) + expect_true(any(grepl("Empty.*id", issues))) + expect_true(any(grepl("Unusual node_type", issues))) + expect_true(any(grepl("missing extension", issues))) + expect_equal(length(issues), 3) +}) + +# Test the exported validation function +test_that("is_valid_put_annotation() correctly identifies valid annotations", { + # Valid annotations + expect_true(is_valid_put_annotation('#put name:"test", label:"Test"')) + expect_true(is_valid_put_annotation('# put name:"test"')) + expect_true(is_valid_put_annotation('#put| name:"test", type:"process"')) + expect_true(is_valid_put_annotation("#put name:'test', label:'Test'")) + + # Valid with complex values + expect_true(is_valid_put_annotation('#put name:"test", desc:"Complex, description", path:"/path/to/file"')) + + # Invalid annotations + expect_false(is_valid_put_annotation("#put")) + expect_false(is_valid_put_annotation("#put invalid")) + expect_false(is_valid_put_annotation("#put name:noQuotes")) + expect_false(is_valid_put_annotation("not a put annotation")) + expect_false(is_valid_put_annotation("")) + expect_false(is_valid_put_annotation("#comment")) +}) + +# Test error handling in parsing +test_that("parse_put_annotation() handles malformed input gracefully", { + # Should not throw errors, just return NULL + expect_silent(expect_null(parse_put_annotation(NULL))) + expect_silent(expect_null(parse_put_annotation(""))) + expect_silent(expect_null(parse_put_annotation("malformed"))) + + # Very long strings should still work + long_value <- paste(rep("a", 1000), collapse = "") + long_annotation <- paste0('#put name:"test", long_value:"', long_value, '"') + result <- parse_put_annotation(long_annotation) + expect_equal(result$name, "test") + expect_equal(nchar(result$long_value), 1000) +}) + +# Test consistency between parsing and validation +test_that("Parsing and validation work together correctly", { + # Valid annotation should parse and validate + valid_annotation <- '#put id:"test_node", label:"Test Node", node_type:"process"' + parsed <- parse_put_annotation(valid_annotation) + expect_false(is.null(parsed)) + expect_true(is_valid_put_annotation(valid_annotation)) + + issues <- validate_annotation(parsed, valid_annotation) + expect_equal(length(issues), 0) + + # Invalid annotation should not parse + invalid_annotation <- "#put invalid syntax" + parsed_invalid <- parse_put_annotation(invalid_annotation) + expect_null(parsed_invalid) + expect_false(is_valid_put_annotation(invalid_annotation)) +}) + +# Performance tests for parsing +test_that("Parsing performs well with many properties", { + # Create annotation with many properties (including id to prevent auto-generation) + many_props <- paste(paste0("prop", 1:50, ':"value', 1:50, '"'), collapse = ", ") + annotation <- paste0("#put id:\"test\", ", many_props) + + # Should parse without issues + start_time <- Sys.time() + result <- parse_put_annotation(annotation) + end_time <- Sys.time() + + expect_equal(length(result), 51) # 50 props + id + expect_lt(as.numeric(end_time - start_time), 1) # Should take less than 1 second + + # Verify some properties + expect_equal(result$id, "test") + expect_equal(result$prop1, "value1") + expect_equal(result$prop50, "value50") +}) diff --git a/tests/testthat/test-put.R b/tests/testthat/test-put.R index 0da7f7f..4eabf86 100644 --- a/tests/testthat/test-put.R +++ b/tests/testthat/test-put.R @@ -1,681 +1,681 @@ -# Test suite for putior package -library(testthat) -library(putior) - -# Test basic functionality -test_that("put() handles basic directory scanning", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_basic") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create test files - r_content <- c( - "# Test R file", - "#put id:\"test_r\", label:\"Test R Node\", node_type:\"process\"", - "x <- 1" - ) - - py_content <- c( - "# Test Python file", - "#put id:\"test_py\", label:\"Test Python Node\", node_type:\"input\"", - "x = 1" - ) - - create_test_file(r_content, "test.R", test_dir) - create_test_file(py_content, "test.py", test_dir) - - # Test the function - result <- put(test_dir) - - expect_s3_class(result, "data.frame") - expect_equal(nrow(result), 2) - expect_true(all(c("file_name", "file_type", "id", "label", "node_type") %in% names(result))) - expect_equal(sort(result$id), c("test_py", "test_r")) - expect_equal(sort(result$file_type), c("py", "r")) -}) - -test_that("put() handles single file processing", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_single") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - "# Single file test", - "#put id:\"single\", label:\"Single Node\", output:\"result.csv\"", - "write.csv(data, 'result.csv')" - ) - - filepath <- create_test_file(content, "single.R", test_dir) - - # Test single file processing - result <- put(filepath) - - expect_s3_class(result, "data.frame") - expect_equal(nrow(result), 1) - expect_equal(result$id, "single") - expect_equal(result$output, "result.csv") -}) - -test_that("put() handles recursive directory scanning", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_recursive") - subdir <- file.path(test_dir, "subdir") - dir.create(test_dir, showWarnings = FALSE) - dir.create(subdir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create files in main directory - create_test_file(c("#put id:\"main\", label:\"Main\""), "main.R", test_dir) - - # Create files in subdirectory - create_test_file(c("#put id:\"sub\", label:\"Sub\""), "sub.py", subdir) - - # Test non-recursive (should find 1) - result_non_recursive <- put(test_dir, recursive = FALSE) - expect_equal(nrow(result_non_recursive), 1) - - # Test recursive (should find 2) - result_recursive <- put(test_dir, recursive = TRUE) - expect_equal(nrow(result_recursive), 2) - expect_true(all(c("main", "sub") %in% result_recursive$id)) -}) - -test_that("put() defaults to recursive scanning", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_default_recursive") - subdir <- file.path(test_dir, "subdir") - dir.create(subdir, recursive = TRUE, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - create_test_file(c("#put id:\"top\", label:\"Top\""), "top.R", test_dir) - create_test_file(c("#put id:\"nested\", label:\"Nested\""), "nested.R", subdir) - - # Default (no explicit recursive arg) should find both files - result <- put(test_dir) - expect_equal(nrow(result), 2) - expect_true(all(c("top", "nested") %in% result$id)) -}) - -test_that("put() includes line numbers when requested", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_lines") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - "# Line 1", - "#put id:\"first\", label:\"First\"", # Line 2 - "# Line 3", - "#put id:\"second\", label:\"Second\"" # Line 4 - ) - - create_test_file(content, "test.R", test_dir) - - # Without line numbers - result_no_lines <- put(test_dir, include_line_numbers = FALSE) - expect_false("line_number" %in% names(result_no_lines)) - - # With line numbers - result_with_lines <- put(test_dir, include_line_numbers = TRUE) - expect_true("line_number" %in% names(result_with_lines)) - expect_equal(result_with_lines$line_number, c(2, 4)) -}) - -# Test annotation parsing -test_that("parse_put_annotation() handles various formats", { - # Basic format - result1 <- parse_put_annotation('#put id:"test", label:"Test Label"') - expect_equal(result1$id, "test") - expect_equal(result1$label, "Test Label") - - # With spaces - result2 <- parse_put_annotation('# put id:"test2", node_type:"process"') - expect_equal(result2$id, "test2") - expect_equal(result2$node_type, "process") - - # With pipe separator - result3 <- parse_put_annotation('#put| id:"test3", input:"data.csv"') - expect_equal(result3$id, "test3") - expect_equal(result3$input, "data.csv") - - # Single quotes - result4 <- parse_put_annotation("#put id:'test4', label:'Single Quotes'") - expect_equal(result4$id, "test4") - expect_equal(result4$label, "Single Quotes") - - # Mixed quotes - result5 <- parse_put_annotation('#put id:"test5", label:\'Mixed Quotes\'') - expect_equal(result5$id, "test5") - expect_equal(result5$label, "Mixed Quotes") -}) - -test_that("parse_put_annotation() handles edge cases", { - # Empty annotation - expect_null(parse_put_annotation("#put")) - expect_null(parse_put_annotation("# put ")) - - # Invalid syntax - expect_null(parse_put_annotation("#put invalid")) - expect_null(parse_put_annotation("#put no quotes")) - - # Not a PUT annotation - expect_null(parse_put_annotation("# Regular comment")) - expect_null(parse_put_annotation("puts something")) -}) - -test_that("put() handles files with no annotations", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_no_annotations") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create file without PUT annotations - content <- c( - "# Regular R file", - "x <- 1:10", - "mean(x)" - ) - - create_test_file(content, "no_annotations.R", test_dir) - - result <- put(test_dir) - expect_s3_class(result, "data.frame") - expect_equal(nrow(result), 0) -}) - -test_that("put() validation works correctly", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_validation") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create file with validation issues - content_with_issues <- c( - "#put label:\"No ID\"", # Missing id - "#put id:\"test\", node_type:\"invalid_type\"", # Invalid node type - "#put id:\"test2\", input:\"noextension\"", # File without extension - "#put id:\"good\", label:\"Good Annotation\", node_type:\"process\"" # Valid - ) - - create_test_file(content_with_issues, "test.R", test_dir) - - # Test with validation enabled (should give warnings) - expect_warning({ - result_with_validation <- put(test_dir, validate = TRUE) - }) - - # Should still return results for valid annotations - expect_gte(nrow(result_with_validation), 1) - - # Test with validation disabled (should not give warnings) - expect_silent({ - result_no_validation <- put(test_dir, validate = FALSE) - }) -}) - -test_that("is_valid_put_annotation() correctly identifies valid annotations", { - # Valid annotations - expect_true(is_valid_put_annotation('#put id:"test", label:"Test"')) - expect_true(is_valid_put_annotation('# put id:"test"')) - expect_true(is_valid_put_annotation('#put| id:"test", type:"process"')) - - # Invalid annotations - expect_false(is_valid_put_annotation("#put")) - expect_false(is_valid_put_annotation("#put invalid")) - expect_false(is_valid_put_annotation("#put id:noQuotes")) - expect_false(is_valid_put_annotation("not a put annotation")) -}) - -# Test error handling -test_that("put() handles errors gracefully", { - # Non-existent directory - expect_error(put("/path/that/does/not/exist"), "Path does not exist") - - # Invalid path type - expect_error(put(123), "'path' must be a single character string") - expect_error(put(c("path1", "path2")), "'path' must be a single character string") - - # Empty directory - temp_dir <- tempdir() - empty_dir <- file.path(temp_dir, "empty_test_dir") - dir.create(empty_dir, showWarnings = FALSE) - on.exit(unlink(empty_dir, recursive = TRUE)) - - expect_warning( - { - result <- put(empty_dir) - }, - "No files matching pattern" - ) - - expect_s3_class(result, "data.frame") - expect_equal(nrow(result), 0) -}) - -test_that("put() handles different file extensions", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_extensions") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create files with different extensions - # Note: The default pattern is "\\.(R|r|py|sql|sh|jl)$" - # Each file type needs its appropriate comment prefix - extensions <- c("R", "py", "sql", "sh", "jl") # Removed lowercase "r" to avoid duplicates - - for (ext in extensions) { - # Use the appropriate comment prefix for each file type - prefix <- get_comment_prefix(ext) - content <- paste0(prefix, "put id:\"test_", ext, "\", label:\"Test ", ext, "\"") - create_test_file(content, paste0("test.", ext), test_dir) - } - - result <- put(test_dir) - expect_equal(nrow(result), length(extensions)) - expect_true(all(tolower(extensions) %in% result$file_type)) -}) - -test_that("put() preserves custom properties", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_custom") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - '#put id:"custom", label:"Custom Node", color:"blue", priority:"high", duration:"5min"' - ) - - create_test_file(content, "custom.R", test_dir) - - result <- put(test_dir) - expect_equal(result$color, "blue") - expect_equal(result$priority, "high") - expect_equal(result$duration, "5min") -}) - -# Performance and stress tests -test_that("put() handles multiple annotations efficiently", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_many") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create file with many annotations - many_annotations <- character() - for (i in 1:50) { - many_annotations <- c( - many_annotations, - paste0('#put id:"node', i, '", label:"Node ', i, '", step:', i) - ) - } - - create_test_file(many_annotations, "many.R", test_dir) - - # Should complete reasonably quickly - start_time <- Sys.time() - result <- put(test_dir) - end_time <- Sys.time() - - expect_equal(nrow(result), 50) - expect_lt(as.numeric(end_time - start_time), 5) # Should take less than 5 seconds -}) - -test_that("put() column ordering is consistent", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_columns") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - '#put zebra:"z", alpha:"a", id:"test", beta:"b"' - ) - - create_test_file(content, "test.R", test_dir) - - result <- put(test_dir) - - # Standard columns should come first - expect_true(which(names(result) == "file_name") < which(names(result) == "alpha")) - expect_true(which(names(result) == "file_type") < which(names(result) == "alpha")) - - # Custom columns should be alphabetically ordered - custom_cols <- names(result)[!names(result) %in% c("file_name", "file_path", "file_type")] - expect_equal(custom_cols, sort(custom_cols)) -}) - -test_that("put() detects duplicate IDs", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_duplicates") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - '#put id:"test_dup", label:"First Duplicate"', - '#put id:"test_dup", label:"Second Duplicate"', - '#put id:"unique_node", label:"Unique Node"' - ) - - create_test_file(content, "test.R", test_dir) - - # Should warn about duplicate IDs - expect_warning({ - result <- put(test_dir, validate = TRUE) - }, "Duplicate node IDs found: test_dup") - - # Should still return all nodes - expect_equal(nrow(result), 3) -}) - -test_that("put() defaults output to file_name when missing", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_output_default") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Create file where output is not specified - content <- c( - '#put label:"Process Step", node_type:"process", input:"data.csv"', - '# No output specified - should default to file name' - ) - - create_test_file(content, "process_script.R", test_dir) - - result <- put(test_dir) - - # Check that output was defaulted to the file name - expect_equal(nrow(result), 1) - expect_equal(result$output, "process_script.R") - expect_equal(result$file_name, "process_script.R") -}) - -# Tests for multiline annotations -test_that("put() handles multiline annotations correctly", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_multiline") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Test file with multiline annotations - multiline_content <- c( - "#put id:\"multi1\", label:\"Multiline Test\", \\", - "# input:\"file1.csv,file2.csv,file3.csv\", \\", - "# output:\"result.csv\"", - "", - "# Some R code here", - "", - "#put id:\"multi2\", \\", - "# label:\"Complex Process\", \\", - "# node_type:\"process\", \\", - "# input:\"data1.csv,data2.csv,data3.csv,data4.csv,data5.csv\", \\", - "# output:\"processed_data.csv\"", - "", - "# More code", - "", - "# Single line annotation for comparison", - "#put id:\"single\", label:\"Single Line\", input:\"test.csv\"" - ) - - test_file <- create_test_file(multiline_content, "multiline_test.R", test_dir) - - # Extract annotations - result <- put(test_dir) - - # Should find 3 annotations - expect_equal(nrow(result), 3) - - # Check first multiline annotation - multi1 <- result[result$id == "multi1", ] - expect_equal(multi1$label, "Multiline Test") - expect_equal(multi1$input, "file1.csv,file2.csv,file3.csv") - expect_equal(multi1$output, "result.csv") - - # Check second multiline annotation - multi2 <- result[result$id == "multi2", ] - expect_equal(multi2$label, "Complex Process") - expect_equal(multi2$node_type, "process") - expect_equal(multi2$input, "data1.csv,data2.csv,data3.csv,data4.csv,data5.csv") - expect_equal(multi2$output, "processed_data.csv") - - # Check single line annotation still works - single <- result[result$id == "single", ] - expect_equal(single$label, "Single Line") - expect_equal(single$input, "test.csv") -}) - -test_that("put() handles edge cases in multiline annotations", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_multiline_edge") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Test edge cases - edge_case_content <- c( - "# Multiline with trailing backslash but no continuation", - "#put id:\"edge1\", label:\"Edge Case 1\" \\", - "", - "# Next annotation immediately after multiline", - "#put id:\"edge2\", label:\"Edge Case 2\" \\", - "#put id:\"edge3\", label:\"This is separate\"", - "", - "# Multiline with empty continuation lines", - "#put id:\"edge4\", label:\"With Empty Lines\", \\", - "#", - "# input:\"test.csv\"", - "", - "# Backslash at end of file", - "#put id:\"edge5\", label:\"End of File\" \\" - ) - - test_file <- create_test_file(edge_case_content, "edge_cases.R", test_dir) - - # Extract annotations - result <- put(test_dir) - - # Should find all annotations (at least 4) - expect_gte(nrow(result), 4) - - # Check that edge cases are handled - edge1 <- result[result$id == "edge1", ] - expect_equal(edge1$label, "Edge Case 1") - - edge3 <- result[result$id == "edge3", ] - expect_equal(edge3$label, "This is separate") - - edge4 <- result[result$id == "edge4", ] - expect_equal(edge4$label, "With Empty Lines") - expect_equal(edge4$input, "test.csv") -}) - -test_that("put() preserves line numbers with multiline annotations", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_multiline_lines") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - content <- c( - "# Line 1", - "#put id:\"first\", label:\"First\" \\", # Line 2 - "# input:\"test.csv\"", # Line 3 - "", - "# Line 5", - "#put id:\"second\", label:\"Second\"" # Line 6 - ) - - test_file <- create_test_file(content, "line_test.R", test_dir) - - # Extract with line numbers - result <- put(test_dir, include_line_numbers = TRUE) - - # Check line numbers point to start of annotation - first <- result[result$id == "first", ] - expect_equal(first$line_number, 2) - - second <- result[result$id == "second", ] - expect_equal(second$line_number, 6) -}) - -test_that("put() handles different multiline syntax variations", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_multiline_syntax") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - # Test different syntax variations - syntax_content <- c( - "# Standard multiline with spaces", - "#put id:\"style1\", label:\"Style 1\" \\", - "# input:\"file1.csv\"", - "", - "# Multiline with pipe separator", - "#put| id:\"style2\", label:\"Style 2\" \\", - "# input:\"file2.csv\"", - "", - "# Multiline with colon separator", - "#put: id:\"style3\", label:\"Style 3\" \\", - "# input:\"file3.csv\"", - "", - "# Backslash with trailing spaces", - "#put id:\"style4\", label:\"Style 4\" \\ ", - "# input:\"file4.csv\"" - ) - - test_file <- create_test_file(syntax_content, "syntax_test.R", test_dir) - - # Extract annotations - result <- put(test_dir) - - # Should find 4 annotations - expect_equal(nrow(result), 4) - - # Verify all styles work - for (i in 1:4) { - style <- result[result$id == paste0("style", i), ] - expect_equal(style$label, paste("Style", i)) - expect_equal(style$input, paste0("file", i, ".csv")) - } -}) - -# ============================================================================= -# S3 Class Tests (putior_workflow) -# ============================================================================= - -test_that("put() returns a putior_workflow object", { - test_dir <- tempfile("putior_s3_test_") - dir.create(test_dir) - on.exit(unlink(test_dir, recursive = TRUE)) - - create_test_file(c('# put label:"Step 1", output:"data.csv"'), "test.R", test_dir) - result <- put(test_dir) - - expect_s3_class(result, "putior_workflow") - expect_s3_class(result, "data.frame") -}) - -test_that("print.putior_workflow shows header with counts", { - test_dir <- tempfile("putior_print_test_") - dir.create(test_dir) - on.exit(unlink(test_dir, recursive = TRUE)) - - create_test_file(c( - '# put label:"Load", node_type:"input", output:"data.csv"', - '# put label:"Process", node_type:"process", input:"data.csv"' - ), "test.R", test_dir) - result <- put(test_dir) - - output <- capture.output(print(result)) - expect_true(any(grepl("putior workflow:", output))) - expect_true(any(grepl("2 node", output))) - expect_true(any(grepl("1 file", output))) - expect_true(any(grepl("Node types:", output))) -}) - -test_that("print.putior_workflow handles empty workflow", { - empty_wf <- putior:::as_putior_workflow(putior:::empty_result_df()) - output <- capture.output(print(empty_wf)) - expect_true(any(grepl("0 nodes", output))) -}) - -test_that("summary.putior_workflow returns structured list", { - test_dir <- tempfile("putior_summary_test_") - dir.create(test_dir) - on.exit(unlink(test_dir, recursive = TRUE)) - - create_test_file(c( - '# put label:"Load", node_type:"input", output:"data.csv"', - '# put label:"Save", node_type:"output", input:"data.csv"' - ), "test.R", test_dir) - result <- put(test_dir) - - output <- capture.output(summ <- summary(result)) - expect_true(any(grepl("putior workflow summary", output))) - expect_true(any(grepl("Nodes: 2", output))) - expect_equal(summ$n_nodes, 2) - expect_equal(summ$n_files, 1) - expect_true(!is.null(summ$node_types)) -}) - -# Tests for sanitize_mermaid_label -test_that("sanitize_mermaid_label wraps labels in quotes", { - result <- putior:::sanitize_mermaid_label("Simple Label") - expect_equal(result, '"Simple Label"') -}) - -test_that("sanitize_mermaid_label escapes internal quotes", { - result <- putior:::sanitize_mermaid_label('Say "hello"') - expect_equal(result, '"Say #quot;hello#quot;"') -}) - -test_that("sanitize_mermaid_label handles empty/NA/NULL", { - expect_equal(putior:::sanitize_mermaid_label(""), "") - expect_true(is.na(putior:::sanitize_mermaid_label(NA))) - expect_null(putior:::sanitize_mermaid_label(NULL)) -}) - -test_that("sanitize_mermaid_label handles special Mermaid characters", { - # Brackets, braces, parens are safe inside quotes - result <- putior:::sanitize_mermaid_label("data [v2] (final)") - expect_equal(result, '"data [v2] (final)"') -}) - -test_that("sanitize_mermaid_label escapes pipe characters", { - result <- putior:::sanitize_mermaid_label("Input | Output") - expect_equal(result, '"Input #124; Output"') - expect_false(grepl("\\|", result)) -}) - -test_that("sanitize_mermaid_label escapes both quotes and pipes", { - result <- putior:::sanitize_mermaid_label('Say "hello" | world') - expect_equal(result, '"Say #quot;hello#quot; #124; world"') -}) - -test_that("put_diagram handles labels with pipe characters", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_pipe_label") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - create_test_file(c( - '# put id:"node1", label:"Parse TABLE|FAN|SPEED", node_type:"process"' - ), "test.R", test_dir) - workflow <- put(test_dir) - diagram <- put_diagram(workflow, output = "raw") - expect_true(grepl("#124;", diagram)) - expect_false(grepl("\\|FAN\\|", diagram)) -}) - -test_that("put_diagram handles labels with special characters", { - temp_dir <- tempdir() - test_dir <- file.path(temp_dir, "putior_test_special_chars") - dir.create(test_dir, showWarnings = FALSE) - on.exit(unlink(test_dir, recursive = TRUE)) - - create_test_file(c( - '# put id:"node1", label:"Load [v2] Data", node_type:"process"' - ), "test.R", test_dir) - workflow <- put(test_dir) - diagram <- put_diagram(workflow, output = "raw") - - # Should not error and should contain the label - expect_true(grepl("Load", diagram)) +# Test suite for putior package +library(testthat) +library(putior) + +# Test basic functionality +test_that("put() handles basic directory scanning", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_basic") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create test files + r_content <- c( + "# Test R file", + "#put id:\"test_r\", label:\"Test R Node\", node_type:\"process\"", + "x <- 1" + ) + + py_content <- c( + "# Test Python file", + "#put id:\"test_py\", label:\"Test Python Node\", node_type:\"input\"", + "x = 1" + ) + + create_test_file(r_content, "test.R", test_dir) + create_test_file(py_content, "test.py", test_dir) + + # Test the function + result <- put(test_dir) + + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 2) + expect_true(all(c("file_name", "file_type", "id", "label", "node_type") %in% names(result))) + expect_equal(sort(result$id), c("test_py", "test_r")) + expect_equal(sort(result$file_type), c("py", "r")) +}) + +test_that("put() handles single file processing", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_single") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + "# Single file test", + "#put id:\"single\", label:\"Single Node\", output:\"result.csv\"", + "write.csv(data, 'result.csv')" + ) + + filepath <- create_test_file(content, "single.R", test_dir) + + # Test single file processing + result <- put(filepath) + + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 1) + expect_equal(result$id, "single") + expect_equal(result$output, "result.csv") +}) + +test_that("put() handles recursive directory scanning", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_recursive") + subdir <- file.path(test_dir, "subdir") + dir.create(test_dir, showWarnings = FALSE) + dir.create(subdir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create files in main directory + create_test_file(c("#put id:\"main\", label:\"Main\""), "main.R", test_dir) + + # Create files in subdirectory + create_test_file(c("#put id:\"sub\", label:\"Sub\""), "sub.py", subdir) + + # Test non-recursive (should find 1) + result_non_recursive <- put(test_dir, recursive = FALSE) + expect_equal(nrow(result_non_recursive), 1) + + # Test recursive (should find 2) + result_recursive <- put(test_dir, recursive = TRUE) + expect_equal(nrow(result_recursive), 2) + expect_true(all(c("main", "sub") %in% result_recursive$id)) +}) + +test_that("put() defaults to recursive scanning", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_default_recursive") + subdir <- file.path(test_dir, "subdir") + dir.create(subdir, recursive = TRUE, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + create_test_file(c("#put id:\"top\", label:\"Top\""), "top.R", test_dir) + create_test_file(c("#put id:\"nested\", label:\"Nested\""), "nested.R", subdir) + + # Default (no explicit recursive arg) should find both files + result <- put(test_dir) + expect_equal(nrow(result), 2) + expect_true(all(c("top", "nested") %in% result$id)) +}) + +test_that("put() includes line numbers when requested", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_lines") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + "# Line 1", + "#put id:\"first\", label:\"First\"", # Line 2 + "# Line 3", + "#put id:\"second\", label:\"Second\"" # Line 4 + ) + + create_test_file(content, "test.R", test_dir) + + # Without line numbers + result_no_lines <- put(test_dir, include_line_numbers = FALSE) + expect_false("line_number" %in% names(result_no_lines)) + + # With line numbers + result_with_lines <- put(test_dir, include_line_numbers = TRUE) + expect_true("line_number" %in% names(result_with_lines)) + expect_equal(result_with_lines$line_number, c(2, 4)) +}) + +# Test annotation parsing +test_that("parse_put_annotation() handles various formats", { + # Basic format + result1 <- parse_put_annotation('#put id:"test", label:"Test Label"') + expect_equal(result1$id, "test") + expect_equal(result1$label, "Test Label") + + # With spaces + result2 <- parse_put_annotation('# put id:"test2", node_type:"process"') + expect_equal(result2$id, "test2") + expect_equal(result2$node_type, "process") + + # With pipe separator + result3 <- parse_put_annotation('#put| id:"test3", input:"data.csv"') + expect_equal(result3$id, "test3") + expect_equal(result3$input, "data.csv") + + # Single quotes + result4 <- parse_put_annotation("#put id:'test4', label:'Single Quotes'") + expect_equal(result4$id, "test4") + expect_equal(result4$label, "Single Quotes") + + # Mixed quotes + result5 <- parse_put_annotation('#put id:"test5", label:\'Mixed Quotes\'') + expect_equal(result5$id, "test5") + expect_equal(result5$label, "Mixed Quotes") +}) + +test_that("parse_put_annotation() handles edge cases", { + # Empty annotation + expect_null(parse_put_annotation("#put")) + expect_null(parse_put_annotation("# put ")) + + # Invalid syntax + expect_null(parse_put_annotation("#put invalid")) + expect_null(parse_put_annotation("#put no quotes")) + + # Not a PUT annotation + expect_null(parse_put_annotation("# Regular comment")) + expect_null(parse_put_annotation("puts something")) +}) + +test_that("put() handles files with no annotations", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_no_annotations") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create file without PUT annotations + content <- c( + "# Regular R file", + "x <- 1:10", + "mean(x)" + ) + + create_test_file(content, "no_annotations.R", test_dir) + + result <- put(test_dir) + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 0) +}) + +test_that("put() validation works correctly", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_validation") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create file with validation issues + content_with_issues <- c( + "#put label:\"No ID\"", # Missing id + "#put id:\"test\", node_type:\"invalid_type\"", # Invalid node type + "#put id:\"test2\", input:\"noextension\"", # File without extension + "#put id:\"good\", label:\"Good Annotation\", node_type:\"process\"" # Valid + ) + + create_test_file(content_with_issues, "test.R", test_dir) + + # Test with validation enabled (should give warnings) + expect_warning({ + result_with_validation <- put(test_dir, validate = TRUE) + }) + + # Should still return results for valid annotations + expect_gte(nrow(result_with_validation), 1) + + # Test with validation disabled (should not give warnings) + expect_silent({ + result_no_validation <- put(test_dir, validate = FALSE) + }) +}) + +test_that("is_valid_put_annotation() correctly identifies valid annotations", { + # Valid annotations + expect_true(is_valid_put_annotation('#put id:"test", label:"Test"')) + expect_true(is_valid_put_annotation('# put id:"test"')) + expect_true(is_valid_put_annotation('#put| id:"test", type:"process"')) + + # Invalid annotations + expect_false(is_valid_put_annotation("#put")) + expect_false(is_valid_put_annotation("#put invalid")) + expect_false(is_valid_put_annotation("#put id:noQuotes")) + expect_false(is_valid_put_annotation("not a put annotation")) +}) + +# Test error handling +test_that("put() handles errors gracefully", { + # Non-existent directory + expect_error(put("/path/that/does/not/exist"), "Path does not exist") + + # Invalid path type + expect_error(put(123), "'path' must be a single character string") + expect_error(put(c("path1", "path2")), "'path' must be a single character string") + + # Empty directory + temp_dir <- tempdir() + empty_dir <- file.path(temp_dir, "empty_test_dir") + dir.create(empty_dir, showWarnings = FALSE) + on.exit(unlink(empty_dir, recursive = TRUE)) + + expect_warning( + { + result <- put(empty_dir) + }, + "No files matching pattern" + ) + + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 0) +}) + +test_that("put() handles different file extensions", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_extensions") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create files with different extensions + # Note: The default pattern is "\\.(R|r|py|sql|sh|jl)$" + # Each file type needs its appropriate comment prefix + extensions <- c("R", "py", "sql", "sh", "jl") # Removed lowercase "r" to avoid duplicates + + for (ext in extensions) { + # Use the appropriate comment prefix for each file type + prefix <- get_comment_prefix(ext) + content <- paste0(prefix, "put id:\"test_", ext, "\", label:\"Test ", ext, "\"") + create_test_file(content, paste0("test.", ext), test_dir) + } + + result <- put(test_dir) + expect_equal(nrow(result), length(extensions)) + expect_true(all(tolower(extensions) %in% result$file_type)) +}) + +test_that("put() preserves custom properties", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_custom") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + '#put id:"custom", label:"Custom Node", color:"blue", priority:"high", duration:"5min"' + ) + + create_test_file(content, "custom.R", test_dir) + + result <- put(test_dir) + expect_equal(result$color, "blue") + expect_equal(result$priority, "high") + expect_equal(result$duration, "5min") +}) + +# Performance and stress tests +test_that("put() handles multiple annotations efficiently", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_many") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create file with many annotations + many_annotations <- character() + for (i in 1:50) { + many_annotations <- c( + many_annotations, + paste0('#put id:"node', i, '", label:"Node ', i, '", step:', i) + ) + } + + create_test_file(many_annotations, "many.R", test_dir) + + # Should complete reasonably quickly + start_time <- Sys.time() + result <- put(test_dir) + end_time <- Sys.time() + + expect_equal(nrow(result), 50) + expect_lt(as.numeric(end_time - start_time), 5) # Should take less than 5 seconds +}) + +test_that("put() column ordering is consistent", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_columns") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + '#put zebra:"z", alpha:"a", id:"test", beta:"b"' + ) + + create_test_file(content, "test.R", test_dir) + + result <- put(test_dir) + + # Standard columns should come first + expect_true(which(names(result) == "file_name") < which(names(result) == "alpha")) + expect_true(which(names(result) == "file_type") < which(names(result) == "alpha")) + + # Custom columns should be alphabetically ordered + custom_cols <- names(result)[!names(result) %in% c("file_name", "file_path", "file_type")] + expect_equal(custom_cols, sort(custom_cols)) +}) + +test_that("put() detects duplicate IDs", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_duplicates") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + '#put id:"test_dup", label:"First Duplicate"', + '#put id:"test_dup", label:"Second Duplicate"', + '#put id:"unique_node", label:"Unique Node"' + ) + + create_test_file(content, "test.R", test_dir) + + # Should warn about duplicate IDs + expect_warning({ + result <- put(test_dir, validate = TRUE) + }, "Duplicate node IDs found: test_dup") + + # Should still return all nodes + expect_equal(nrow(result), 3) +}) + +test_that("put() defaults output to file_name when missing", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_output_default") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Create file where output is not specified + content <- c( + '#put label:"Process Step", node_type:"process", input:"data.csv"', + '# No output specified - should default to file name' + ) + + create_test_file(content, "process_script.R", test_dir) + + result <- put(test_dir) + + # Check that output was defaulted to the file name + expect_equal(nrow(result), 1) + expect_equal(result$output, "process_script.R") + expect_equal(result$file_name, "process_script.R") +}) + +# Tests for multiline annotations +test_that("put() handles multiline annotations correctly", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_multiline") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Test file with multiline annotations + multiline_content <- c( + "#put id:\"multi1\", label:\"Multiline Test\", \\", + "# input:\"file1.csv,file2.csv,file3.csv\", \\", + "# output:\"result.csv\"", + "", + "# Some R code here", + "", + "#put id:\"multi2\", \\", + "# label:\"Complex Process\", \\", + "# node_type:\"process\", \\", + "# input:\"data1.csv,data2.csv,data3.csv,data4.csv,data5.csv\", \\", + "# output:\"processed_data.csv\"", + "", + "# More code", + "", + "# Single line annotation for comparison", + "#put id:\"single\", label:\"Single Line\", input:\"test.csv\"" + ) + + test_file <- create_test_file(multiline_content, "multiline_test.R", test_dir) + + # Extract annotations + result <- put(test_dir) + + # Should find 3 annotations + expect_equal(nrow(result), 3) + + # Check first multiline annotation + multi1 <- result[result$id == "multi1", ] + expect_equal(multi1$label, "Multiline Test") + expect_equal(multi1$input, "file1.csv,file2.csv,file3.csv") + expect_equal(multi1$output, "result.csv") + + # Check second multiline annotation + multi2 <- result[result$id == "multi2", ] + expect_equal(multi2$label, "Complex Process") + expect_equal(multi2$node_type, "process") + expect_equal(multi2$input, "data1.csv,data2.csv,data3.csv,data4.csv,data5.csv") + expect_equal(multi2$output, "processed_data.csv") + + # Check single line annotation still works + single <- result[result$id == "single", ] + expect_equal(single$label, "Single Line") + expect_equal(single$input, "test.csv") +}) + +test_that("put() handles edge cases in multiline annotations", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_multiline_edge") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Test edge cases + edge_case_content <- c( + "# Multiline with trailing backslash but no continuation", + "#put id:\"edge1\", label:\"Edge Case 1\" \\", + "", + "# Next annotation immediately after multiline", + "#put id:\"edge2\", label:\"Edge Case 2\" \\", + "#put id:\"edge3\", label:\"This is separate\"", + "", + "# Multiline with empty continuation lines", + "#put id:\"edge4\", label:\"With Empty Lines\", \\", + "#", + "# input:\"test.csv\"", + "", + "# Backslash at end of file", + "#put id:\"edge5\", label:\"End of File\" \\" + ) + + test_file <- create_test_file(edge_case_content, "edge_cases.R", test_dir) + + # Extract annotations + result <- put(test_dir) + + # Should find all annotations (at least 4) + expect_gte(nrow(result), 4) + + # Check that edge cases are handled + edge1 <- result[result$id == "edge1", ] + expect_equal(edge1$label, "Edge Case 1") + + edge3 <- result[result$id == "edge3", ] + expect_equal(edge3$label, "This is separate") + + edge4 <- result[result$id == "edge4", ] + expect_equal(edge4$label, "With Empty Lines") + expect_equal(edge4$input, "test.csv") +}) + +test_that("put() preserves line numbers with multiline annotations", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_multiline_lines") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + content <- c( + "# Line 1", + "#put id:\"first\", label:\"First\" \\", # Line 2 + "# input:\"test.csv\"", # Line 3 + "", + "# Line 5", + "#put id:\"second\", label:\"Second\"" # Line 6 + ) + + test_file <- create_test_file(content, "line_test.R", test_dir) + + # Extract with line numbers + result <- put(test_dir, include_line_numbers = TRUE) + + # Check line numbers point to start of annotation + first <- result[result$id == "first", ] + expect_equal(first$line_number, 2) + + second <- result[result$id == "second", ] + expect_equal(second$line_number, 6) +}) + +test_that("put() handles different multiline syntax variations", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_multiline_syntax") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + # Test different syntax variations + syntax_content <- c( + "# Standard multiline with spaces", + "#put id:\"style1\", label:\"Style 1\" \\", + "# input:\"file1.csv\"", + "", + "# Multiline with pipe separator", + "#put| id:\"style2\", label:\"Style 2\" \\", + "# input:\"file2.csv\"", + "", + "# Multiline with colon separator", + "#put: id:\"style3\", label:\"Style 3\" \\", + "# input:\"file3.csv\"", + "", + "# Backslash with trailing spaces", + "#put id:\"style4\", label:\"Style 4\" \\ ", + "# input:\"file4.csv\"" + ) + + test_file <- create_test_file(syntax_content, "syntax_test.R", test_dir) + + # Extract annotations + result <- put(test_dir) + + # Should find 4 annotations + expect_equal(nrow(result), 4) + + # Verify all styles work + for (i in 1:4) { + style <- result[result$id == paste0("style", i), ] + expect_equal(style$label, paste("Style", i)) + expect_equal(style$input, paste0("file", i, ".csv")) + } +}) + +# ============================================================================= +# S3 Class Tests (putior_workflow) +# ============================================================================= + +test_that("put() returns a putior_workflow object", { + test_dir <- tempfile("putior_s3_test_") + dir.create(test_dir) + on.exit(unlink(test_dir, recursive = TRUE)) + + create_test_file(c('# put label:"Step 1", output:"data.csv"'), "test.R", test_dir) + result <- put(test_dir) + + expect_s3_class(result, "putior_workflow") + expect_s3_class(result, "data.frame") +}) + +test_that("print.putior_workflow shows header with counts", { + test_dir <- tempfile("putior_print_test_") + dir.create(test_dir) + on.exit(unlink(test_dir, recursive = TRUE)) + + create_test_file(c( + '# put label:"Load", node_type:"input", output:"data.csv"', + '# put label:"Process", node_type:"process", input:"data.csv"' + ), "test.R", test_dir) + result <- put(test_dir) + + output <- capture.output(print(result)) + expect_true(any(grepl("putior workflow:", output))) + expect_true(any(grepl("2 node", output))) + expect_true(any(grepl("1 file", output))) + expect_true(any(grepl("Node types:", output))) +}) + +test_that("print.putior_workflow handles empty workflow", { + empty_wf <- putior:::as_putior_workflow(putior:::empty_result_df()) + output <- capture.output(print(empty_wf)) + expect_true(any(grepl("0 nodes", output))) +}) + +test_that("summary.putior_workflow returns structured list", { + test_dir <- tempfile("putior_summary_test_") + dir.create(test_dir) + on.exit(unlink(test_dir, recursive = TRUE)) + + create_test_file(c( + '# put label:"Load", node_type:"input", output:"data.csv"', + '# put label:"Save", node_type:"output", input:"data.csv"' + ), "test.R", test_dir) + result <- put(test_dir) + + output <- capture.output(summ <- summary(result)) + expect_true(any(grepl("putior workflow summary", output))) + expect_true(any(grepl("Nodes: 2", output))) + expect_equal(summ$n_nodes, 2) + expect_equal(summ$n_files, 1) + expect_true(!is.null(summ$node_types)) +}) + +# Tests for sanitize_mermaid_label +test_that("sanitize_mermaid_label wraps labels in quotes", { + result <- putior:::sanitize_mermaid_label("Simple Label") + expect_equal(result, '"Simple Label"') +}) + +test_that("sanitize_mermaid_label escapes internal quotes", { + result <- putior:::sanitize_mermaid_label('Say "hello"') + expect_equal(result, '"Say #quot;hello#quot;"') +}) + +test_that("sanitize_mermaid_label handles empty/NA/NULL", { + expect_equal(putior:::sanitize_mermaid_label(""), "") + expect_true(is.na(putior:::sanitize_mermaid_label(NA))) + expect_null(putior:::sanitize_mermaid_label(NULL)) +}) + +test_that("sanitize_mermaid_label handles special Mermaid characters", { + # Brackets, braces, parens are safe inside quotes + result <- putior:::sanitize_mermaid_label("data [v2] (final)") + expect_equal(result, '"data [v2] (final)"') +}) + +test_that("sanitize_mermaid_label escapes pipe characters", { + result <- putior:::sanitize_mermaid_label("Input | Output") + expect_equal(result, '"Input #124; Output"') + expect_false(grepl("\\|", result)) +}) + +test_that("sanitize_mermaid_label escapes both quotes and pipes", { + result <- putior:::sanitize_mermaid_label('Say "hello" | world') + expect_equal(result, '"Say #quot;hello#quot; #124; world"') +}) + +test_that("put_diagram handles labels with pipe characters", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_pipe_label") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + create_test_file(c( + '# put id:"node1", label:"Parse TABLE|FAN|SPEED", node_type:"process"' + ), "test.R", test_dir) + workflow <- put(test_dir) + diagram <- put_diagram(workflow, output = "raw") + expect_true(grepl("#124;", diagram)) + expect_false(grepl("\\|FAN\\|", diagram)) +}) + +test_that("put_diagram handles labels with special characters", { + temp_dir <- tempdir() + test_dir <- file.path(temp_dir, "putior_test_special_chars") + dir.create(test_dir, showWarnings = FALSE) + on.exit(unlink(test_dir, recursive = TRUE)) + + create_test_file(c( + '# put id:"node1", label:"Load [v2] Data", node_type:"process"' + ), "test.R", test_dir) + workflow <- put(test_dir) + diagram <- put_diagram(workflow, output = "raw") + + # Should not error and should contain the label + expect_true(grepl("Load", diagram)) }) \ No newline at end of file diff --git a/tests/testthat/test-put_diagram.R b/tests/testthat/test-put_diagram.R index efa8887..d8395da 100644 --- a/tests/testthat/test-put_diagram.R +++ b/tests/testthat/test-put_diagram.R @@ -1,936 +1,936 @@ -# Test suite for put_diagram() function -library(testthat) - -# Helper function to create test workflow data -create_test_workflow <- function() { - data.frame( - file_name = c("01_load.R", "02_process.py", "03_analyze.R"), - file_type = c("r", "py", "r"), - id = c("load_data", "process_data", "analyze_results"), - label = c("Load Raw Data", "Process Data", "Analyze Results"), - node_type = c("input", "process", "output"), - input = c(NA, "raw_data.csv", "processed_data.csv"), - output = c("raw_data.csv", "processed_data.csv", "final_report.html"), - stringsAsFactors = FALSE - ) -} - -# Helper function to create test workflow with terminal outputs -create_test_workflow_with_terminal <- function() { - data.frame( - file_name = c("load.R", "process.R"), - file_type = c("r", "r"), - id = c("load", "process"), - label = c("Load Data", "Process Data"), - node_type = c("input", "process"), - input = c(NA, "data.csv"), - output = c("data.csv", "final_results.csv"), # final_results.csv is terminal - stringsAsFactors = FALSE - ) -} - -test_that("put_diagram() creates basic mermaid diagram", { - workflow <- create_test_workflow() - - # Capture output - result <- capture.output({ - diagram_code <- put_diagram(workflow) - }) - - # Check that mermaid code block is created - expect_true(any(grepl("```mermaid", result))) - expect_true(any(grepl("flowchart TD", result))) - expect_true(any(grepl("```", result))) - - # Check that diagram code is returned invisibly - expect_true(is.character(diagram_code)) - expect_true(grepl("flowchart TD", diagram_code)) -}) - -test_that("put_diagram() handles different directions", { - workflow <- create_test_workflow() - - # Test different directions - directions <- c("TD", "LR", "BT", "RL") - - for (dir in directions) { - diagram_code <- put_diagram(workflow, direction = dir, output = "none") - expect_true(grepl(paste0("flowchart ", dir), diagram_code)) - } -}) - -test_that("put_diagram() handles different node label options", { - workflow <- create_test_workflow() - - # Test ID labels (node_labels = "name" shows IDs) - diagram_id <- put_diagram(workflow, node_labels = "name", output = "none") - expect_true(grepl("load_data", diagram_id)) - - # Test description labels - diagram_label <- put_diagram(workflow, node_labels = "label", output = "none") - expect_true(grepl("Load Raw Data", diagram_label)) - - # Test both labels - diagram_both <- put_diagram(workflow, node_labels = "both", output = "none") - expect_true(grepl("load_data: Load Raw Data", diagram_both)) -}) - -test_that("put_diagram() generates correct node shapes", { - workflow <- create_test_workflow() - diagram_code <- put_diagram(workflow, output = "none") - - # Check shapes for different node types - expect_true(grepl("\\(\\[.*\\]\\)", diagram_code)) # Stadium shape for input - expect_true(grepl("\\[.*\\]", diagram_code)) # Rectangle for process - expect_true(grepl("\\[\\[.*\\]\\]", diagram_code)) # Subroutine for output -}) - -test_that("put_diagram() creates connections based on file flow", { - workflow <- create_test_workflow() - diagram_code <- put_diagram(workflow, output = "none") - - # Should have connections between nodes that share files - expect_true(grepl("load_data --> process_data", diagram_code)) - expect_true(grepl("process_data --> analyze_results", diagram_code)) -}) - -test_that("put_diagram() shows file names on connections when requested", { - workflow <- create_test_workflow() - - # Without file names - diagram_no_files <- put_diagram(workflow, show_files = FALSE, output = "none") - expect_true(grepl("load_data --> process_data", diagram_no_files)) - expect_false(grepl("raw_data.csv", diagram_no_files)) - - # With file names - diagram_with_files <- put_diagram(workflow, show_files = TRUE, output = "none") - expect_true(grepl("\\|raw_data.csv\\|", diagram_with_files)) - expect_true(grepl("\\|processed_data.csv\\|", diagram_with_files)) -}) - -test_that("put_diagram() adds styling when requested", { - workflow <- create_test_workflow() - - # With styling - diagram_styled <- put_diagram(workflow, style_nodes = TRUE, output = "none") - expect_true(grepl("classDef inputStyle", diagram_styled)) - expect_true(grepl("classDef processStyle", diagram_styled)) - expect_true(grepl("classDef outputStyle", diagram_styled)) - - # Without styling - diagram_unstyled <- put_diagram(workflow, style_nodes = FALSE, output = "none") - expect_false(grepl("classDef", diagram_unstyled)) -}) - -test_that("put_diagram() includes title when provided", { - workflow <- create_test_workflow() - - diagram_with_title <- put_diagram(workflow, title = "My Workflow", output = "none") - expect_true(grepl("title: My Workflow", diagram_with_title)) - - diagram_no_title <- put_diagram(workflow, title = NULL, output = "none") - expect_false(grepl("title:", diagram_no_title)) -}) - -test_that("put_diagram() handles file output", { - workflow <- create_test_workflow() - temp_file <- tempfile(fileext = ".md") - - # Test file output - expect_message( - { - put_diagram(workflow, output = "file", file = temp_file) - }, - "Diagram saved to" - ) - - # Check file was created and has correct content - expect_true(file.exists(temp_file)) - - content <- readLines(temp_file) - expect_true(any(grepl("```mermaid", content))) - expect_true(any(grepl("flowchart TD", content))) - - # Clean up - unlink(temp_file) -}) - -test_that("put_diagram() validates input", { - # Empty workflow - expect_error(put_diagram(data.frame()), "empty.*0 rows") - - # Missing required columns - bad_workflow <- data.frame(x = 1, y = 2) - expect_error(put_diagram(bad_workflow), "missing required column") - - # All IDs missing - workflow_no_ids <- data.frame( - id = c(NA, "", NA), - file_name = c("a.R", "b.py", "c.R"), - stringsAsFactors = FALSE - ) - expect_error(put_diagram(workflow_no_ids), "No valid workflow nodes found") -}) - -test_that("sanitize_node_id() works correctly", { - # Test normal names - expect_equal(sanitize_node_id("load_data"), "load_data") - expect_equal(sanitize_node_id("process-data"), "process_data") - expect_equal(sanitize_node_id("analyze.results"), "analyze_results") - - # Test names starting with numbers - expect_equal(sanitize_node_id("1_load_data"), "node_1_load_data") - - # Test special characters - expect_equal(sanitize_node_id("load@data#1"), "load_data_1") -}) - -test_that("split_file_list() handles various inputs", { - # Single file - expect_equal(split_file_list("file.csv"), "file.csv") - - # Multiple files - expect_equal(split_file_list("file1.csv,file2.json"), c("file1.csv", "file2.json")) - - # With spaces - expect_equal( - split_file_list("file1.csv, file2.json, file3.txt"), - c("file1.csv", "file2.json", "file3.txt") - ) - - # Empty string - expect_equal(split_file_list(""), character(0)) - expect_equal(split_file_list(NA), character(0)) -}) - -test_that("get_node_shape() returns correct shapes", { - expect_equal(get_node_shape("input"), c("([", "])")) - expect_equal(get_node_shape("process"), c("[", "]")) - expect_equal(get_node_shape("output"), c("[[", "]]")) - expect_equal(get_node_shape("decision"), c("{", "}")) - expect_equal(get_node_shape(NA), c("[", "]")) - expect_equal(get_node_shape("unknown"), c("[", "]")) -}) - -test_that("put_diagram() handles complex workflows", { - # Create workflow with multiple inputs/outputs and various node types - complex_workflow <- data.frame( - file_name = c("collect.py", "clean.R", "analyze.R", "report.R", "decide.R"), - id = c("collect", "clean", "analyze", "report", "decide"), - label = c("Collect Data", "Clean Data", "Analyze", "Generate Report", "Make Decision"), - node_type = c("input", "process", "process", "output", "decision"), - input = c(NA, "raw.csv", "clean.csv", "analysis.rds", "analysis.rds"), - output = c("raw.csv", "clean.csv", "analysis.rds", "report.html", "decision.json"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(complex_workflow, output = "none") - - # Should handle all node types - expect_true(grepl("collect", diagram_code)) - expect_true(grepl("clean", diagram_code)) - expect_true(grepl("analyze", diagram_code)) - expect_true(grepl("report", diagram_code)) - expect_true(grepl("decide", diagram_code)) - - # Should have appropriate connections - expect_true(grepl("collect --> clean", diagram_code)) - expect_true(grepl("clean --> analyze", diagram_code)) -}) - -test_that("put_diagram() handles workflows with no connections", { - # Independent nodes with no shared files - independent_workflow <- data.frame( - file_name = c("task1.R", "task2.R", "task3.R"), - id = c("task1", "task2", "task3"), - label = c("Task 1", "Task 2", "Task 3"), - node_type = c("process", "process", "process"), - input = c("file1.csv", "file2.csv", "file3.csv"), - output = c("out1.csv", "out2.csv", "out3.csv"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(independent_workflow, output = "none") - - # Should still create valid diagram with nodes - expect_true(grepl("flowchart TD", diagram_code)) - expect_true(grepl("task1", diagram_code)) - expect_true(grepl("task2", diagram_code)) - expect_true(grepl("task3", diagram_code)) - - # Should not have connections - expect_false(grepl("-->", diagram_code)) -}) - -# Mock clipr for clipboard tests -test_that("put_diagram() handles clipboard output", { - workflow <- create_test_workflow() - - # Mock clipr not being available - if (!requireNamespace("clipr", quietly = TRUE)) { - expect_warning( - { - result <- capture.output({ - put_diagram(workflow, output = "clipboard") - }) - }, - "clipr package not available" - ) - - # Should fall back to console output - expect_true(any(grepl("```mermaid", result))) - } -}) - -# ============================================================================ -# Artifact functionality tests -# ============================================================================ - -test_that("put_diagram() supports show_artifacts parameter", { - workflow <- create_test_workflow_with_terminal() - - # Test that show_artifacts parameter is accepted - expect_no_error({ - diagram_simple <- put_diagram(workflow, output = "none") - diagram_artifacts <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - }) - - # Diagrams should be different - diagram_simple <- put_diagram(workflow, output = "none") - diagram_artifacts <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - expect_false(identical(diagram_simple, diagram_artifacts)) -}) - -test_that("create_artifact_nodes() identifies data files correctly", { - workflow <- create_test_workflow_with_terminal() - artifacts <- create_artifact_nodes(workflow) - - # Should identify final_results.csv as an artifact (terminal output) - expect_true(nrow(artifacts) > 0) - expect_true("final_results.csv" %in% artifacts$file_name) - expect_true(all(artifacts$node_type == "artifact")) - expect_true(all(artifacts$is_artifact == TRUE)) -}) - -test_that("artifact nodes have correct shape", { - # Test artifact node shape - expect_equal(get_node_shape("artifact"), c("[(", ")]")) -}) - -test_that("show_artifacts creates artifact nodes in diagram", { - workflow <- create_test_workflow_with_terminal() - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - - # Should contain artifact node for terminal output - expect_true(grepl("artifact_final_results_csv", diagram_code)) - expect_true(grepl("final_results\\.csv", diagram_code)) -}) - -test_that("show_artifacts creates correct connections", { - workflow <- create_test_workflow_with_terminal() - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - - # Should have BOTH script-to-script AND script-to-artifact connections - # Script-to-script connection (preserved from simple mode) - expect_true(grepl("load --> process", diagram_code)) - - # Script-to-artifact and artifact-to-script connections - expect_true(grepl("load --> artifact_data_csv", diagram_code)) - expect_true(grepl("artifact_data_csv --> process", diagram_code)) - expect_true(grepl("process --> artifact_final_results_csv", diagram_code)) -}) - -test_that("artifact styling is applied correctly", { - workflow <- create_test_workflow_with_terminal() - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - - # Should contain artifact styling - expect_true(grepl("classDef artifactStyle", diagram_code)) - expect_true(grepl("class artifact_", diagram_code)) -}) - -test_that("show_artifacts with show_files shows file labels", { - workflow <- create_test_workflow_with_terminal() - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, show_files = TRUE, output = "none") - - # Should have file labels on connections - expect_true(grepl("-->\\|.*\\|", diagram_code)) # Should have |file| labels -}) - -test_that("show_artifacts works with different themes", { - workflow <- create_test_workflow_with_terminal() - - # Test multiple themes - themes <- c("light", "dark", "github", "minimal", "auto") - - for (theme in themes) { - expect_no_error({ - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, theme = theme, output = "none") - expect_true(grepl("artifact", diagram_code)) - }) - } -}) - -test_that("show_artifacts handles workflow with no artifacts", { - # Create workflow where all outputs are script files - workflow <- data.frame( - file_name = c("main.R", "utils.R"), - id = c("main", "utils"), - label = c("Main", "Utils"), - node_type = c("process", "input"), - input = c("utils.R", NA), - output = c("results.txt", "utils.R"), # utils.R is script, not artifact - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, output = "none") - - # Should handle gracefully and show results.txt artifact - expect_true(grepl("artifact_results_txt", diagram_code)) -}) - -test_that("show_artifacts combines with existing node_type styling", { - workflow <- create_test_workflow_with_terminal() - diagram_code <- put_diagram(workflow, show_artifacts = TRUE, style_nodes = TRUE, output = "none") - - # Should have both script node styling and artifact styling - expect_true(grepl("classDef inputStyle", diagram_code)) - expect_true(grepl("classDef processStyle", diagram_code)) - expect_true(grepl("classDef artifactStyle", diagram_code)) -}) - -# ============================================================================ -# Workflow boundary functionality tests -# ============================================================================ - -test_that("put_diagram() supports show_workflow_boundaries parameter", { - workflow <- data.frame( - file_name = c("start.R", "process.R", "end.R"), - id = c("start_node", "process_node", "end_node"), - label = c("Start", "Process", "End"), - node_type = c("start", "process", "end"), - stringsAsFactors = FALSE - ) - - # Test that show_workflow_boundaries parameter is accepted - expect_no_error({ - diagram_boundaries <- put_diagram(workflow, show_workflow_boundaries = TRUE, output = "none") - diagram_no_boundaries <- put_diagram(workflow, show_workflow_boundaries = FALSE, output = "none") - }) - - # Diagrams should be different - diagram_boundaries <- put_diagram(workflow, show_workflow_boundaries = TRUE, output = "none") - diagram_no_boundaries <- put_diagram(workflow, show_workflow_boundaries = FALSE, output = "none") - expect_false(identical(diagram_boundaries, diagram_no_boundaries)) -}) - -test_that("workflow boundaries show special icons when enabled", { - workflow <- data.frame( - file_name = c("start.R", "end.R"), - id = c("start_node", "end_node"), - label = c("Pipeline Start", "Pipeline End"), - node_type = c("start", "end"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(workflow, show_workflow_boundaries = TRUE, output = "none") - - # Should contain start and end nodes with special styling - expect_true(grepl("startStyle", diagram_code)) # Special start styling - expect_true(grepl("endStyle", diagram_code)) # Special end styling -}) - -test_that("workflow boundaries hide icons when disabled", { - workflow <- data.frame( - file_name = c("start.R", "end.R"), - id = c("start_node", "end_node"), - label = c("Pipeline Start", "Pipeline End"), - node_type = c("start", "end"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(workflow, show_workflow_boundaries = FALSE, output = "none") - - # Should NOT contain special styling when boundaries are disabled - expect_false(grepl("startStyle", diagram_code)) # No start styling - expect_false(grepl("endStyle", diagram_code)) # No end styling -}) - -test_that("workflow boundary styling is applied when enabled", { - workflow <- data.frame( - file_name = c("start.R", "end.R"), - id = c("start_node", "end_node"), - label = c("Start", "End"), - node_type = c("start", "end"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(workflow, show_workflow_boundaries = TRUE, output = "none") - - # Should contain start and end styling - expect_true(grepl("classDef startStyle", diagram_code)) - expect_true(grepl("classDef endStyle", diagram_code)) - expect_true(grepl("class start_node startStyle", diagram_code)) - expect_true(grepl("class end_node endStyle", diagram_code)) -}) - -test_that("workflow boundary styling is hidden when disabled", { - workflow <- data.frame( - file_name = c("start.R", "end.R"), - id = c("start_node", "end_node"), - label = c("Start", "End"), - node_type = c("start", "end"), - stringsAsFactors = FALSE - ) - - diagram_code <- put_diagram(workflow, show_workflow_boundaries = FALSE, output = "none") - - # Should NOT contain start and end styling when boundaries are disabled - expect_false(grepl("classDef startStyle", diagram_code)) - expect_false(grepl("classDef endStyle", diagram_code)) -}) - -test_that("get_node_shape handles workflow boundaries correctly", { - # With boundaries enabled, should get stadium shapes (no special characters) - expect_equal(get_node_shape("start", TRUE), c("([", "])")) - expect_equal(get_node_shape("end", TRUE), c("([", "])")) - - # With boundaries disabled, should get regular shapes - expect_equal(get_node_shape("start", FALSE), c("([", "])")) - expect_equal(get_node_shape("end", FALSE), c("([", "])")) -}) - -test_that("workflow boundaries work with different themes", { - workflow <- data.frame( - file_name = c("start.R", "end.R"), - id = c("start_node", "end_node"), - label = c("Start", "End"), - node_type = c("start", "end"), - stringsAsFactors = FALSE - ) - - # Test multiple themes with workflow boundaries - themes <- c("light", "dark", "github", "minimal", "auto") - - for (theme in themes) { - expect_no_error({ - diagram_code <- put_diagram(workflow, show_workflow_boundaries = TRUE, theme = theme, output = "none") - expect_true(grepl("startStyle", diagram_code)) - expect_true(grepl("endStyle", diagram_code)) - }) - } -}) - -# ============================================================================ -# Metadata Display (show_source_info) Tests - GitHub Issue #3 -# ============================================================================ - -test_that("show_source_info adds file names inline", { - workflow <- create_test_workflow() - - # Without source info - diagram_no_info <- put_diagram(workflow, show_source_info = FALSE, output = "none") - expect_false(grepl("