├── .Rbuildignore ├── .Rprofile ├── .dev ├── .gitkeep ├── CRAN │ └── prepare-for-release.R ├── docker │ ├── r-plumber │ │ └── Dockerfile │ └── r-test │ │ └── Dockerfile └── hooks │ └── tidy-styling.R ├── .dockerignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yml │ └── pkgdown.yml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── globals.R ├── plumber-add_service.R ├── plumber-use_microservice.R ├── utils-DockerCompose.R ├── utils.R └── zzz.R ├── README.Rmd ├── README.md ├── codecov.yml ├── docker-compose.yml ├── inst ├── CITATION ├── REFERENCES.bib ├── configurations │ ├── fs.yml │ ├── openapi.yml │ └── plumber.yml ├── endpoints │ ├── plumber-utility.R │ └── plumber-{route_name}.R ├── entrypoints │ ├── plumber-background.R │ └── plumber-foreground.R ├── snippets │ ├── demo-RestRserve-package.R │ └── enable-docker-at-startup.R └── templates │ ├── R │ ├── globals.R │ ├── plumber-add_service.R │ ├── plumber-use_microservice.R │ ├── utils-DockerCompose.R │ ├── utils.R │ └── zzz.R │ ├── docker-compose.yml │ ├── inst │ ├── configurations │ │ ├── fs.yml │ │ ├── openapi.yml │ │ └── plumber.yml │ ├── endpoints │ │ ├── plumber-utility.R │ │ └── plumber-{route_name}.R │ └── entrypoints │ │ ├── plumber-background.R │ │ └── plumber-foreground.R │ └── tests │ ├── testthat.R │ └── testthat │ ├── _api │ └── 127.0.0.1-8080 │ │ └── route_name │ │ └── read_table-a26694.R │ ├── helpers-xyz.R │ ├── test-endpoint-plumber-{route_name}.R │ └── test-plumber-microservice.R ├── microservices.Rproj ├── pkgdown ├── .gitignore ├── _pkgdown.yml ├── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico └── logo.png ├── tests ├── testthat.R └── testthat │ ├── _api │ └── 127.0.0.1-8080 │ │ └── route_name │ │ ├── list_tables.json │ │ ├── read_table-7376d4.json │ │ ├── read_table-a26694.R │ │ └── write_table-10996a-POST.json │ ├── helpers-xyz.R │ ├── test-endpoint-plumber-{route_name}.R │ └── test-plumber-microservice.R └── vignettes ├── .gitignore ├── _common.R ├── articles ├── 01-introduction.Rmd ├── 02-plumber.Rmd └── microservices.Rmd ├── details ├── add_service.Rmd └── use_microservice.Rmd └── excerpts └── _setup-rudimentary-microservice.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | ^\..*/ 3 | ^\.Rproj\.user$ 4 | .*_cache/ 5 | ^data-raw/ 6 | ^inst/snippets/ 7 | ^renv/ 8 | ^vignettes/ 9 | ^revdep/ 10 | 11 | # Files 12 | ^\..* 13 | ^.*\.Rproj$ 14 | ^codecov\.yml$ 15 | ^cran-comments\.md$ 16 | ^CRAN-RELEASE$ 17 | ^CRAN-SUBMISSION$ 18 | ^README\.Rmd$ 19 | ^LICENSE\.md$ 20 | ^vignettes/_common.R$ 21 | ^R/utils-microservice.R$ 22 | 23 | # Docker 24 | ^docker-compose\.yml$ 25 | ^R/utils-DockerCompose\.R$ 26 | 27 | # pkgdown$ 28 | ^docs$ 29 | ^pkgdown$ 30 | ^docker_boot\.service$ 31 | 32 | -------------------------------------------------------------------------------- /.Rprofile: -------------------------------------------------------------------------------- 1 | assign(".Rprofile", new.env(), envir = globalenv()) 2 | 3 | # .First ------------------------------------------------------------------ 4 | .First <- function(){ 5 | try(if(testthat::is_testing()) return()) 6 | suppressWarnings(try(readRenviron(".Renviron"), silent = TRUE)) 7 | .Rprofile$utils$copy_package() 8 | 9 | # Package Management System 10 | Date <- as.character(read.dcf("DESCRIPTION", "Date")) 11 | URL <- if(is.na(Date)) "https://cran.rstudio.com/" else paste0("https://mran.microsoft.com/snapshot/", Date) 12 | options(repos = URL) 13 | } 14 | 15 | # .Last ------------------------------------------------------------------- 16 | .Last <- function(){ 17 | try(if(testthat::is_testing()) return()) 18 | .Rprofile$utils$copy_package() 19 | 20 | unlink("./renv", recursive = TRUE) 21 | try(system('docker-compose down'), silent = TRUE) 22 | } 23 | 24 | # Docker ------------------------------------------------------------------ 25 | .Rprofile$docker$browse_url <- function(service){ 26 | path_script <- tempfile("system-", fileext = ".R") 27 | job_name <- paste("Testing", as.character(read.dcf('DESCRIPTION', 'Package')), "in a Docker Container") 28 | define_service <- paste0("service = c(", paste0(paste0("'",service,"'"), collapse = ", "),")") 29 | define_service <- if(is.null(service)) "service = NULL" else define_service 30 | writeLines(c( 31 | "source('./R/utils-DockerCompose.R')", 32 | define_service, 33 | "DockerCompose$new()$browse_url(service)"), path_script) 34 | .Rprofile$utils$run_script(path_script, job_name) 35 | } 36 | 37 | .Rprofile$docker$start <- function(service = NULL){ 38 | .Rprofile$utils$copy_package() 39 | path_script <- tempfile("system-", fileext = ".R") 40 | job_name <- paste("Testing", as.character(read.dcf('DESCRIPTION', 'Package')), "in a Docker Container") 41 | define_service <- paste0("service <- c(", paste0(paste0("'",service,"'"), collapse = ", "),")") 42 | define_service <- if(is.null(service)) "service = NULL" else define_service 43 | writeLines(c( 44 | "source('./R/utils-DockerCompose.R')", 45 | define_service, 46 | "DockerCompose$new()$start(service)"), path_script) 47 | .Rprofile$utils$run_script(path_script, job_name) 48 | } 49 | 50 | .Rprofile$docker$stop <- function(){ 51 | path_script <- tempfile("system-", fileext = ".R") 52 | job_name <- paste("Testing", as.character(read.dcf('DESCRIPTION', 'Package')), "in a Docker Container") 53 | writeLines(c("source('./R/utils-DockerCompose.R'); DockerCompose$new()$stop()"), path_script) 54 | .Rprofile$utils$run_script(path_script, job_name) 55 | } 56 | 57 | .Rprofile$docker$restart <- function(service = NULL){ 58 | .Rprofile$utils$copy_package() 59 | path_script <- tempfile("system-", fileext = ".R") 60 | job_name <- paste("Testing", as.character(read.dcf('DESCRIPTION', 'Package')), "in a Docker Container") 61 | define_service <- paste0("service <- c(", paste0(paste0("'",service,"'"), collapse = ", "),")") 62 | define_service <- if(is.null(service)) "service = NULL" else define_service 63 | writeLines(c( 64 | "source('./R/utils-DockerCompose.R')", 65 | define_service, 66 | "DockerCompose$new()$restart(service)"), path_script) 67 | .Rprofile$utils$run_script(path_script, job_name) 68 | } 69 | 70 | .Rprofile$docker$reset <- function(){ 71 | path_script <- tempfile("system-", fileext = ".R") 72 | job_name <- paste("Testing", as.character(read.dcf('DESCRIPTION', 'Package')), "in a Docker Container") 73 | writeLines(c("source('./R/utils-DockerCompose.R'); DockerCompose$new()$reset()"), path_script) 74 | .Rprofile$utils$run_script(path_script, job_name) 75 | } 76 | 77 | # pkgdown ----------------------------------------------------------------- 78 | .Rprofile$pkgdown$browse <- function(name){ 79 | if(missing(name)){ 80 | path <- "./docs" 81 | name <- "index.html" 82 | } else { 83 | path <- "./docs/articles" 84 | name <- match.arg(name, list.files(path, "*.html")) 85 | } 86 | try(browseURL(stringr::str_glue('{path}/{name}', path = path, name = name))) 87 | invisible() 88 | } 89 | 90 | .Rprofile$pkgdown$create <- function(){ 91 | path_script <- tempfile("system-", fileext = ".R") 92 | job_name <- "Rendering Package Website" 93 | 94 | writeLines(c( 95 | "devtools::document()", 96 | "rmarkdown::render('README.Rmd', 'md_document')", 97 | "unlink(usethis::proj_path('docs'), TRUE, TRUE)", 98 | paste0("try(detach('package:",read.dcf("DESCRIPTION", "Package")[[1]], "', unload = TRUE, force = TRUE))"), 99 | "pkgdown::build_site(devel = FALSE, lazy = FALSE)" 100 | ), path_script) 101 | 102 | .Rprofile$utils$run_script(path_script, job_name) 103 | } 104 | 105 | .Rprofile$pkgdown$update <- function(){ 106 | path_script <- tempfile("system-", fileext = ".R") 107 | job_name <- "Rendering Package Website" 108 | 109 | writeLines(c( 110 | "devtools::document()", 111 | "rmarkdown::render('README.Rmd', 'md_document')", 112 | paste0("try(detach('package:",read.dcf("DESCRIPTION", "Package")[[1]], "', unload = TRUE, force = TRUE))"), 113 | "pkgdown::build_site(devel = TRUE, lazy = TRUE)" 114 | ), path_script) 115 | 116 | .Rprofile$utils$run_script(path_script, job_name) 117 | } 118 | 119 | # Utils ------------------------------------------------------------------- 120 | .Rprofile$utils$run_script <- function(path, name){ 121 | withr::with_envvar( 122 | c(TESTTHAT = "true"), 123 | rstudioapi::jobRunScript( 124 | path = path, 125 | name = name, 126 | workingDir = ".", 127 | importEnv = FALSE, 128 | exportEnv = "" 129 | )) 130 | invisible() 131 | } 132 | 133 | .Rprofile$utils$copy_package <- function(path = file.path(getwd(), "inst", "templates")){ 134 | target_temporary <- tempfile() 135 | 136 | files <- list.files(getwd(), pattern = ".(R|r|yml)$", all.files = FALSE, recursive = TRUE) 137 | files <- Filter(function(x) !grepl("vignettes|pkgdown|docs|snippets|codecov|revdep", x), files) 138 | folders <- unique(dirname(files)) 139 | 140 | suppressWarnings({ 141 | unlink(path, recursive = TRUE) 142 | dir.create(path, recursive = TRUE) 143 | 144 | sapply(file.path(target_temporary, folders), dir.create, recursive = TRUE) 145 | file.copy(from = file.path(getwd(), files), to = file.path(target_temporary, files), recursive = TRUE) 146 | 147 | sapply(file.path(path, folders), dir.create, recursive = TRUE) 148 | file.copy(from = file.path(target_temporary, files), to = file.path(path, files), recursive = TRUE) 149 | 150 | invisible() 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /.dev/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/.dev/.gitkeep -------------------------------------------------------------------------------- /.dev/CRAN/prepare-for-release.R: -------------------------------------------------------------------------------- 1 | # Configuration ----------------------------------------------------------- 2 | Sys.setenv(`_R_DEPENDS_ONLY` = "true") 3 | 4 | 5 | # Setup ------------------------------------------------------------------- 6 | remotes::install_cran(c("devtools", "urlchecker", "rhub", "revdepcheck")) 7 | # remotes::install_github("r-lib/revdepcheck@main") 8 | 9 | 10 | # Steps ------------------------------------------------------------------- 11 | # devtools::build_readme() 12 | urlchecker::url_check() 13 | devtools::check(remote = TRUE, manual = TRUE) 14 | devtools::check_win_devel() 15 | rhub::check_for_cran() 16 | revdepcheck::revdep_check(num_workers = 8) 17 | # revdepcheck::revdep_reset(pkg = ".") 18 | 19 | 20 | # Deployment -------------------------------------------------------------- 21 | # devtools::submit_cran() 22 | -------------------------------------------------------------------------------- /.dev/docker/r-plumber/Dockerfile: -------------------------------------------------------------------------------- 1 | # R Package Development: Plumber ----------------------------------------------- 2 | FROM rstudio/plumber:v1.0.0 3 | 4 | # Setup ------------------------------------------------------------------------ 5 | RUN apt-get update -qq && apt-get -y --no-install-recommends install \ 6 | dos2unix \ 7 | libcurl4-openssl-dev \ 8 | libssl-dev \ 9 | libxt-dev \ 10 | libxml2-dev \ 11 | libcairo2-dev \ 12 | libsqlite-dev \ 13 | libmariadbd-dev \ 14 | libmariadbclient-dev \ 15 | libpq-dev \ 16 | libsodium-dev \ 17 | libssh2-1-dev \ 18 | libsasl2-dev \ 19 | unixodbc-dev \ 20 | libsqliteodbc \ 21 | libfreetype6-dev \ 22 | libpng-dev \ 23 | libtiff5-dev \ 24 | libjpeg-dev \ 25 | git \ 26 | && apt-get clean && rm -rf /var/lib/apt/lists/* 27 | 28 | # Install Project Dependencies ------------------------------------------------- 29 | ARG R_REPOS=\'https://mran.microsoft.com/snapshot/2021-08-10\' 30 | RUN touch .Rprofile .Renviron 31 | RUN echo "options(repos = ${R_REPOS})" >> .Rprofile 32 | RUN R -q -e "if(!require(remotes)) install.packages('remotes')" 33 | RUN echo "require(remotes)" >> .Rprofile 34 | 35 | RUN R -q -e "update_packages()" 36 | RUN R -q -e "install_cran('devtools')" 37 | RUN R -q -e "install_cran('microservices', dependencies = TRUE)" 38 | 39 | 40 | COPY ./DESCRIPTION ./DESCRIPTION 41 | RUN R -q -e "install_deps(dependencies = TRUE)" 42 | 43 | COPY . ./app 44 | WORKDIR ./app 45 | 46 | # Spin Up Microserver ---------------------------------------------------------- 47 | RUN sed -i -e 's/host:.*/host: 0.0.0.0/g' ./inst/configurations/plumber.yml 48 | CMD ["/app/inst/entrypoints/plumber-foreground.R"] 49 | -------------------------------------------------------------------------------- /.dev/docker/r-test/Dockerfile: -------------------------------------------------------------------------------- 1 | # R Package Development: Core -------------------------------------------------- 2 | FROM tidylab/microservice:4.2.1 3 | 4 | # Install Project Dependencies ------------------------------------------------- 5 | COPY ./DESCRIPTION ./DESCRIPTION 6 | RUN R -q -e "install_deps(dependencies = 'Depends')" 7 | RUN R -q -e "install_deps(dependencies = 'Imports')" 8 | RUN R -q -e "install_deps(dependencies = 'Suggests')" 9 | 10 | # R Package Development: Testing------------------------------------------------ 11 | RUN touch .Renviron .Rprofile 12 | RUN echo "" > .Rprofile 13 | 14 | # Prepare Package Files -------------------------------------------------------- 15 | ARG R_USER=./home/rstudio/ 16 | ARG R_PACKAGE_NAME=rproject 17 | COPY . ${R_USER}/${R_PACKAGE_NAME} 18 | RUN cp .Rprofile ${R_USER}/${R_PACKAGE_NAME} 19 | RUN cp .env ${R_USER}/.Renviron 20 | RUN echo "_R_CHECK_SYSTEM_CLOCK_=0" >> .Rprofile 21 | WORKDIR ${R_USER}/${R_PACKAGE_NAME} 22 | 23 | # Test-Suite ------------------------------------------------------------------- 24 | RUN R -q -e "devtools::document()" 25 | RUN R -q -e "devtools::check(error_on = 'note')" 26 | RUN R -q -e "devtools::load_all(export_all = FALSE, helpers = FALSE);\ 27 | testthat::test_dir('./tests/testthat', stop_on_failure = TRUE)" 28 | 29 | # Teardown --------------------------------------------------------------------- 30 | ENTRYPOINT /bin/bash 31 | -------------------------------------------------------------------------------- /.dev/hooks/tidy-styling.R: -------------------------------------------------------------------------------- 1 | # usethis::use_tidy_github_actions() 2 | # 3 | # usethis::create_tidy_package(path, copyright_holder = NULL) 4 | # 5 | usethis::use_tidy_description() 6 | # 7 | # usethis::use_tidy_eval() 8 | # 9 | # usethis::use_tidy_contributing() 10 | # 11 | # usethis::use_tidy_support() 12 | # 13 | # usethis::use_tidy_issue_template() 14 | # 15 | # usethis::use_tidy_coc() 16 | # 17 | # usethis::use_tidy_github() 18 | # 19 | # usethis::use_tidy_style(strict = TRUE) 20 | # 21 | # usethis::use_tidy_release_test_env() 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .*/ 3 | cache/ 4 | inst/docs/ 5 | man/ 6 | vignettes/_cache/ 7 | revdep/ 8 | 9 | # Files 10 | .* 11 | !.Rbuildignore 12 | ^docker-compose.yml$ 13 | 14 | # Demo 15 | ./R/demo-* 16 | ./tests/testthat/test-demo-* 17 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | on: 4 | push: 5 | # branches: 6 | # - master 7 | pull_request: 8 | branches: 9 | - master 10 | - develop 11 | 12 | name: R-CMD-check 13 | 14 | jobs: 15 | R-CMD-check: 16 | runs-on: ${{ matrix.config.os }} 17 | 18 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | config: 24 | - {os: windows-latest, r: 'release'} 25 | - {os: macOS-latest, r: 'release'} 26 | - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 27 | 28 | env: 29 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 30 | R_COMPILE_AND_INSTALL_PACKAGES: always 31 | RSPM: ${{ matrix.config.rspm }} 32 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 33 | NOT_CRAN: false 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: r-lib/actions/setup-r@v1 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | 42 | - uses: r-lib/actions/setup-pandoc@v1 43 | 44 | - name: Prepare 45 | run: | 46 | echo "utils::chooseCRANmirror(graphics=FALSE, ind = 1)" > .Rprofile 47 | Rscript -e "Date <- as.character(read.dcf('DESCRIPTION', 'Date')); 48 | URL <- if(is.na(Date)) 'https://cran.rstudio.com/' else paste0('https://mran.microsoft.com/snapshot/', Date); 49 | Rprofile <- file('.Rprofile', open = 'wt'); 50 | writeLines('.libPaths(Sys.getenv(\'R_LIBS_USER\'))', Rprofile); 51 | writeLines('require(remotes)', Rprofile); 52 | writeLines(paste0('options(repos = \'', URL, '\')'), Rprofile); 53 | close(Rprofile)" 54 | Rscript -e "if(!'remotes' %in% rownames(utils::installed.packages())) utils::install.packages('remotes')" 55 | - name: Query dependencies 56 | run: | 57 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 58 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 59 | shell: Rscript {0} 60 | 61 | - name: Cache R packages 62 | if: runner.os != 'Windows' 63 | uses: actions/cache@v2 64 | with: 65 | path: ${{ env.R_LIBS_USER }} 66 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 67 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 68 | 69 | - name: Install system dependencies 70 | if: runner.os == 'Linux' 71 | run: | 72 | while read -r cmd 73 | do 74 | eval sudo $cmd 75 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 76 | sudo apt-get install build-essential libcurl4-gnutls-dev libxml2-dev libssl-dev libgit2-dev 77 | 78 | - name: Install dependencies 79 | run: | 80 | remotes::install_deps(dependencies = TRUE) 81 | remotes::install_cran(c("devtools", "rcmdcheck", "rmarkdown")) 82 | shell: Rscript {0} 83 | 84 | - name: Check 85 | env: 86 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 87 | run: | 88 | devtools::document() 89 | rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 90 | shell: Rscript {0} 91 | 92 | - name: Upload check results 93 | if: failure() 94 | uses: actions/upload-artifact@main 95 | with: 96 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 97 | path: check 98 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - release/* 6 | 7 | name: pkgdown 8 | 9 | jobs: 10 | pkgdown: 11 | runs-on: macOS-latest 12 | env: 13 | GITHUB_PAT: ${{ secrets.PKGDOWN_PAT }} 14 | _R_S3_METHOD_REGISTRATION_NOTE_OVERWRITES_: false 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: r-lib/actions/setup-r@master 19 | 20 | - uses: r-lib/actions/setup-pandoc@master 21 | 22 | - uses: r-lib/actions/setup-tinytex@v1 23 | 24 | - name: Install system dependencies 25 | run: | 26 | brew install harfbuzz fribidi libgit2 27 | rm .Rprofile 28 | 29 | - name: Prepare 30 | run: | 31 | echo "utils::chooseCRANmirror(graphics=FALSE, ind = 1)" > .Rprofile 32 | Rscript -e "if(!'remotes' %in% rownames(utils::installed.packages())) utils::install.packages('remotes')" 33 | 34 | - name: Query dependencies 35 | run: | 36 | pkgs <- remotes::dev_package_deps(dependencies = TRUE); print(pkgs[order(pkgs$package),]) 37 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 38 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 39 | shell: Rscript {0} 40 | 41 | - name: Cache R packages 42 | uses: actions/cache@v1 43 | with: 44 | path: ${{ env.R_LIBS_USER }} 45 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 46 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 47 | 48 | - name: Install dependencies 49 | run: | 50 | sapply(c("tidyverse", "devtools", "pkgdown", "covr"), function(x) try(remotes::install_cran(x))) 51 | remotes::update_packages("pkgdown") 52 | remotes::install_deps(dependencies = TRUE) 53 | shell: Rscript {0} 54 | 55 | - name: Install package 56 | run: R CMD INSTALL . 57 | 58 | - name: Deploy website 59 | run: | 60 | git config --local user.email "actions@github.com" 61 | git config --local user.name "GitHub Actions" 62 | Rscript -e 'devtools::document()' 63 | Rscript -e 'rmarkdown::render("README.Rmd", "md_document")' 64 | Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 65 | Rscript -e 'covr::codecov(type = "all")' 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | docs/ 3 | man/ 4 | renv/ 5 | revdep/ 6 | *_cache/ 7 | 8 | # Files 9 | CRAN-RELEASE 10 | cran-comments.md 11 | .Renviron 12 | .Rproj.user 13 | .Rhistory 14 | .RData 15 | .Ruserdata 16 | *.png 17 | renv.lock 18 | docs 19 | CRAN-SUBMISSION 20 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Type: Package 2 | Package: microservices 3 | Title: Breakdown a Monolithic Application to a Suite of Services 4 | Version: 0.2.0 5 | Date: 2022-06-23 6 | Authors@R: c( 7 | person("Harel", "Lustiger", , "tidylab@gmail.com", role = c("aut", "cre"), 8 | comment = c(ORCID = "0000-0003-2953-9598")), 9 | person("Tidylab", role = c("cph", "fnd")) 10 | ) 11 | Maintainer: Harel Lustiger 12 | Description: 'Microservice' architectural style is an approach to 13 | developing a single application as a suite of small services, each 14 | running in its own process and communicating with lightweight 15 | mechanisms, often an 'HTTP' resource 'API'. These services are built 16 | around business capabilities and independently deployable by fully 17 | automated deployment machinery. There is a bare minimum of centralized 18 | management of these services, which may be written in different 19 | programming languages and use different data storage technologies. 20 | License: MIT + file LICENSE 21 | URL: https://tidylab.github.io/microservices/, 22 | https://github.com/tidylab/microservices 23 | BugReports: https://github.com/tidylab/microservices/issues 24 | Depends: 25 | R (>= 4.2) 26 | Imports: 27 | config, 28 | desc, 29 | dplyr, 30 | fs, 31 | glue, 32 | purrr, 33 | withr 34 | Suggests: 35 | future, 36 | httptest (>= 3.3.0), 37 | httr, 38 | jsonlite, 39 | pkgload, 40 | plumber (>= 1.0.0), 41 | promises, 42 | testthat (>= 2.3.0), 43 | usethis (>= 1.3.0) 44 | Config/testthat/edition: 3 45 | Encoding: UTF-8 46 | Language: en-GB 47 | Roxygen: list(markdown = TRUE, r6 = TRUE) 48 | RoxygenNote: 7.2.0 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2021 2 | COPYRIGHT HOLDER: Harel Lustiger 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Harel Lustiger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(add_service) 4 | export(use_microservice) 5 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # microservices 0.2.0 2 | 3 | * Added support for multi-session 4 | * Added support for running a microservice at launch on AWS 5 | 6 | # microservices 0.1.2 7 | 8 | * Renamed functions to be congruent with `tidylab` R packages collection 9 | 10 | # microservices 0.1.1 11 | 12 | * Added alias: 13 | * `use_plumber_microservice()` and `use_microservice()` are synonyms 14 | * `add_plumber_service()` and `add_service()` are synonyms 15 | 16 | # microservices 0.1.0 17 | 18 | * New features: 19 | * `use_plumber_microservice()` 20 | * `add_plumber_service()` 21 | -------------------------------------------------------------------------------- /R/globals.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | ## Required for {tidyverse} 3 | utils::globalVariables(".") 4 | 5 | -------------------------------------------------------------------------------- /R/plumber-add_service.R: -------------------------------------------------------------------------------- 1 | #' @title Add a Service Route to the Microservice 2 | #' @description Expose additional set of services on a separate URL. 3 | #' 4 | #' @inheritParams use_microservice 5 | #' @param name (`character`) what is the service route name? For example, if 6 | #' \code{name} = "repository" then the set of services would become available 7 | #' at `http://127.0.0.1:8080/repository/`. 8 | #' 9 | #' @details 10 | #' ```{r child = "vignettes/details/add_service.Rmd"} 11 | #' ```` 12 | #' @return No return value, called for side effects. 13 | #' @family plumber microservice 14 | #' @export 15 | #' @examples 16 | #' path <- tempfile() 17 | #' dir.create(path, showWarnings = FALSE, recursive = TRUE) 18 | #' use_microservice(path) 19 | #' 20 | #' add_service(path, name = "repository") 21 | #' 22 | #' list.files(path, recursive = TRUE) 23 | add_service <- function(path = ".", name, overwrite = FALSE){ 24 | name <- gsub(" |\\.", "-", tolower(name)) 25 | .add_service$assert_microservice_exists(path) 26 | .add_service$mount_service(path, name) 27 | .add_service$endpoint_test(path, name, overwrite = overwrite) 28 | .add_service$endpoint_script(path, name, overwrite = overwrite) 29 | 30 | invisible() 31 | } 32 | 33 | 34 | # low-level functions ----------------------------------------------------- 35 | .add_service <- new.env() 36 | 37 | .add_service$assert_microservice_exists <- function(path){ 38 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 39 | files <- config::get("use_microservice", file = file_fs)$files$add 40 | 41 | missing_files <- names(Filter(isFALSE, sapply(file.path(path, files), file.exists))) 42 | if(length(missing_files) == 0) return(invisible()) 43 | stop("\nDid you call use_microservice()? Couldn't find:\n", paste("-->", missing_files, collapse = "\n")) 44 | } 45 | 46 | .add_service$mount_service <- function(path, name){ 47 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 48 | files <- config::get("use_microservice", file = file_fs)$files$add 49 | file <- file.path(path ,files[grepl("plumber-foreground.R", files)]) 50 | 51 | content <- readLines(file) 52 | content <- content[!grepl("route_name", content)] 53 | 54 | row_index <- which.max(grepl("root\\$mount", content)) 55 | new_row <- paste0("root$mount('", name, "', plumber::Plumber$new(file.path(endpoint_path, 'plumber-", name, ".R')))") 56 | 57 | content <- append(content, new_row, row_index) 58 | writeLines(content, file) 59 | invisible() 60 | } 61 | 62 | .add_service$endpoint_test <- function(path, name, overwrite){ 63 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 64 | files <- config::get("add_service", file = file_fs)$files$add 65 | file <- files[grepl("test-", files)] 66 | root <- system.file("templates", package = "microservices", mustWork = TRUE) 67 | 68 | source_file <- file.path(root, file) 69 | content <- readLines(source_file) 70 | content <- gsub("route_name", name, content) 71 | 72 | target_file <- file.path(path, glue::glue(file, route_name = name)) 73 | if(file.exists(target_file) & isFALSE(overwrite)) return() 74 | 75 | dir.create(dirname(target_file), F, T); file.create(target_file) 76 | writeLines(content, target_file) 77 | 78 | invisible() 79 | } 80 | 81 | .add_service$endpoint_script <- function(path, name, overwrite){ 82 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 83 | files <- config::get("add_service", file = file_fs)$files$add 84 | file <- files[grepl("/endpoints/", files)] 85 | root <- system.file("templates", package = "microservices", mustWork = TRUE) 86 | 87 | source_file <- file.path(root, file) 88 | content <- readLines(source_file) 89 | content <- gsub("route_name", name, content) 90 | 91 | target_file <- file.path(path, glue::glue(file, route_name = name)) 92 | if(file.exists(target_file) & isFALSE(overwrite)) return() 93 | 94 | dir.create(dirname(target_file), F, T); file.create(target_file) 95 | writeLines(content, target_file) 96 | 97 | invisible() 98 | } 99 | -------------------------------------------------------------------------------- /R/plumber-use_microservice.R: -------------------------------------------------------------------------------- 1 | #' @title Use a plumber Microservice in an R Project 2 | #' @description 3 | #' Lay the infrastructure for a microservice. That includes unit test, 4 | #' dependency packages, configuration file, entrypoints and utility endpoint. 5 | #' 6 | #' @param path (`character`) Where is the project root folder? 7 | #' @param overwrite (`logical`) Should existing destination files be overwritten? 8 | #' 9 | #' @details 10 | #' ```{r child = "vignettes/details/use_microservice.Rmd"} 11 | #' ```` 12 | #' 13 | #' @return No return value, called for side effects. 14 | #' @family plumber microservice 15 | #' @export 16 | #' @examples 17 | #' path <- tempfile() 18 | #' use_microservice(path) 19 | #' 20 | #' list.files(path, recursive = TRUE) 21 | #' 22 | #' cat(read.dcf(file.path(path, "DESCRIPTION"), "Imports")) 23 | #' cat(read.dcf(file.path(path, "DESCRIPTION"), "Suggests")) 24 | use_microservice <- function(path = ".", overwrite = FALSE){ 25 | dir.create(path, FALSE, TRUE) 26 | .use_microservice$add_files(path = path, overwrite = overwrite) 27 | .use_microservice$update_files(path = path) 28 | .use_microservice$add_dependencies(path = path) 29 | invisible() 30 | } 31 | 32 | 33 | # low-level functions ----------------------------------------------------- 34 | .use_microservice <- new.env() 35 | 36 | .use_microservice$add_files <- function(path, overwrite){ 37 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 38 | files <- config::get("use_microservice", file = file_fs)$files$add 39 | 40 | for(file in files){ 41 | file_source <- fs::path_package("microservices", "templates", gsub("plumber-utility\\.R$", "plumber-{route_name}.R", file)) 42 | file_target <- file.path(path, file) 43 | dir.create(dirname(file_target), showWarnings = FALSE, recursive = TRUE) 44 | file.copy(from = file_source, to = file_target, overwrite = overwrite) 45 | } 46 | } 47 | 48 | .use_microservice$update_files <- function(path, overwrite){ 49 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 50 | files <- config::get("use_microservice", file = file_fs)$files$update 51 | 52 | for(file in files){ 53 | file_source <- system.file(package = "microservices", "templates", file, mustWork = TRUE) 54 | file_target <- file.path(path, file) 55 | dir.create(dirname(file_target), showWarnings = FALSE, recursive = TRUE) 56 | if(file.does.not.exist(file_target)) file.create(file_target) 57 | content <- readLines(file_source) 58 | write(content, file_target, append = TRUE) 59 | } 60 | 61 | invisible() 62 | } 63 | 64 | .use_microservice$add_dependencies <- function(path){ 65 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 66 | dependencies <- config::get("use_microservice", file = file_fs)$dependencies |> as.data.frame() 67 | 68 | desc <- .utils$get_description_obj(path = path) 69 | desc$set_deps(dependencies)$write(file.path(path, "DESCRIPTION")) 70 | } 71 | 72 | -------------------------------------------------------------------------------- /R/utils-DockerCompose.R: -------------------------------------------------------------------------------- 1 | # DockerCompose ----------------------------------------------------------- 2 | #' @title Use a docker-compose.yml File 3 | #' @description 4 | #' GIVEN a \code{docker-compose.yml}, 5 | #' WEHN \code{DockerCompose} is instantiated, 6 | #' THEN the resulting object gives access to Docker commands. 7 | #' @param service (`character`) Service name in \code{docker-compose.yml}. 8 | #' @param field (`character`) Field name in \code{docker-compose.yml}. 9 | #' @param slug (`character`) URL slug (e.g. \code{shiny-app-name}). 10 | #' @family docker 11 | #' @export 12 | DockerCompose <- R6::R6Class(# nocov start 13 | classname = "DockerCompose", 14 | cloneable = FALSE, 15 | lock_objects = FALSE, 16 | public = list( 17 | # Public Methods ------------------------------------------------------- 18 | #' @description 19 | #' Initialize a DockerCompose object 20 | #' @param path_docker_compose (`character`) Path to docker-compose file. 21 | initialize = function(path_docker_compose = "./docker-compose.yml"){ 22 | stopifnot(file.exists(path_docker_compose)) 23 | private$path_docker_compose <- path_docker_compose 24 | private$composition <- yaml::read_yaml(path_docker_compose, eval.expr = FALSE) 25 | invisible(self) 26 | }, 27 | #' @description 28 | #' Get a value from a service 29 | #' @examples \donttest{\dontrun{DockerCompose$new()$get("shinyserver", "ports")}} 30 | get = function(service, field) DockerCompose$funs$get(self, private, service, field), 31 | #' @description 32 | #' Create and start containers. 33 | start = function(service = NULL) DockerCompose$funs$start(self, private, service), 34 | #' @description 35 | #' Stop containers. 36 | stop = function() DockerCompose$funs$stop(self, private), 37 | #' @description 38 | #' Restart containers. 39 | restart = function(service = NULL) DockerCompose$funs$restart(self, private, service), 40 | #' @description 41 | #' Stop and remove containers, networks, images and volumes. 42 | reset = function() DockerCompose$funs$reset(self, private), 43 | #' @description 44 | #' Load URL into an HTML Browser 45 | browse_url = function(service, slug = "") DockerCompose$funs$browse_url(self, private, service, slug) 46 | ),# end public 47 | private = list( 48 | path_docker_compose = c(), 49 | composition = list() 50 | ) 51 | )# nocov end 52 | DockerCompose$funs <- new.env() 53 | 54 | # Public Methods ---------------------------------------------------------- 55 | DockerCompose$funs$reset <- function(self, private){ 56 | system <- DockerCompose$funs$system 57 | docker_commands <- c( 58 | "docker-compose down", 59 | "docker system prune -f", 60 | "docker volume prune -f", 61 | "docker network prune -f", 62 | "docker rmi -f $(docker images -a -q)" 63 | ) 64 | sapply(docker_commands, function(x) try(system(x, wait = TRUE))) 65 | invisible(self) 66 | } 67 | 68 | DockerCompose$funs$restart <- function(self, private, service){ 69 | system <- DockerCompose$funs$system 70 | DockerCompose$funs$stop(self, private) 71 | DockerCompose$funs$start(self, private, service) 72 | invisible(self) 73 | } 74 | 75 | DockerCompose$funs$start <- function(self, private, service){ 76 | is.not.null <- Negate(is.null) 77 | if(is.not.null(service)){ 78 | service <- match.arg(service, names(private$composition$services), several.ok = TRUE) 79 | } 80 | 81 | system <- DockerCompose$funs$system 82 | docker_command <- glue::glue("docker-compose up -d --build {services}", services = paste0(service, collapse = " ")) 83 | system(docker_command, wait = TRUE) 84 | invisible(self) 85 | } 86 | 87 | DockerCompose$funs$stop <- function(self, private){ 88 | system <- DockerCompose$funs$system 89 | docker_command <- glue::glue("docker-compose down") 90 | system(docker_command, wait = TRUE) 91 | invisible(self) 92 | } 93 | 94 | DockerCompose$funs$browse_url <- function(self, private, service, slug){ 95 | service <- match.arg(service, names(private$composition$services)) 96 | url <- "localhost" 97 | port <- stringr::str_remove(self$get(service, "ports"), ":.*") 98 | if(length(port) == 0) port <- "8080" 99 | address <- glue::glue("http://{url}:{port}/{slug}", url = "localhost", port = port, slug = slug) 100 | try(browseURL(utils::URLencode(address))) 101 | return(self) 102 | } 103 | 104 | DockerCompose$funs$get <- function(self, private, service, field){ 105 | stopifnot(!missing(field)) 106 | service <- match.arg(service, names(private$composition$services)) 107 | private$composition$services[[service]][[field]] 108 | } 109 | 110 | # Helpers ----------------------------------------------------------------- 111 | DockerCompose$funs$system <- function(command, ...){ message("\033[43m\033[44m",command,"\033[43m\033[49m") ; base::system(command, ...) } 112 | DockerCompose$funs$escape_character <- function(x){ if(is.character(x)) paste0('"', x, '"') else x } 113 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' @noRd 2 | imports_field <- function() { 3 | config::get 4 | desc::desc 5 | dplyr::filter 6 | fs::path 7 | glue::glue 8 | purrr::walk 9 | withr::with_options 10 | } 11 | 12 | # Negates ----------------------------------------------------------------- 13 | file.does.not.exist <- Negate(base::file.exists) 14 | is.not.null <- Negate(base::is.null) 15 | 16 | # Helpers ----------------------------------------------------------------- 17 | .utils <- new.env() 18 | 19 | .utils$get_description_obj <- function(path){ 20 | desc_file <- file.path(path, "DESCRIPTION") 21 | dir.create(path, showWarnings = FALSE, recursive = TRUE) 22 | if(file.does.not.exist(desc_file)) withr::with_dir(path, desc::description$new("!new")$write(file = "DESCRIPTION")) 23 | return(desc::description$new(desc_file)) 24 | } 25 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onAttach <- function(lib, pkg,...){#nocov start 2 | options( 3 | usethis.quiet = TRUE 4 | ) 5 | 6 | if(interactive()) packageStartupMessage( 7 | paste( 8 | "\n\033[44m\033[37m", 9 | "\nWelcome to microservices", 10 | "\nMore information, vignettes, and guides are available on the microservices project website:", 11 | "\nhttps://tidylab.github.io/microservices/", 12 | "\n\033[39m\033[49m", 13 | sep="") 14 | ) 15 | }#nocov end 16 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | bibliography: [./inst/REFERENCES.bib] 4 | biblio-style: apalike 5 | link-citations: yes 6 | nocite: | 7 | @Fowler2014 8 | @Newman2015 9 | editor_options: 10 | canonical: true 11 | markdown: 12 | wrap: 80 13 | references: 14 | location: section 15 | --- 16 | 17 | ```{r, include = FALSE} 18 | source(file.path(usethis::proj_get(), "vignettes", "_common.R")) 19 | ``` 20 | 21 | # `microservices` 22 | 23 | 24 | 25 | [![CRAN 26 | status](https://www.r-pkg.org/badges/version/microservices)](https://CRAN.R-project.org/package=microservices) 27 | [![R build 28 | status](https://github.com/tidylab/microservices/workflows/R-CMD-check/badge.svg)](https://github.com/tidylab/microservices/actions) 29 | [![codecov](https://codecov.io/gh/tidylab/microservices/branch/master/graph/badge.svg?token=ZLBYE2NIWF)](https://app.codecov.io/gh/tidylab/microservices) 30 | 31 | 32 | 33 | `r read.dcf("DESCRIPTION", "Title")[1]` 34 | 35 | ```{r, echo = FALSE, out.width = "100%"} 36 | knitr::include_graphics("https://i.imgur.com/nZ5jw2g.png") 37 | ``` 38 | 39 | ## Introduction 40 | 41 | `r read.dcf("DESCRIPTION", "Description")[1]` 42 | 43 | ## Should I use microservices? 44 | 45 | As a start, ask yourself if a microservice architecture is a good choice for the 46 | system you're working on? 47 | 48 | ::: {.alert .alert-danger} 49 | **Caution**: If you do not plan to deploy a system into production, then you do 50 | not need `microservices`. 51 | ::: 52 | 53 | The microservice architecture entails costs and provides benefits. Making lots 54 | of independent parts work together incurs complexities. Management, maintenance, 55 | support, and testing costs add up in the system. Many software development 56 | efforts would be better off if they don't use it. 57 | 58 | If you plan to deploy a system into production, then consider the following: 59 | 60 | - Favour a monolith over microservices for simple applications. Monolith can 61 | get you to market quicker than microservices. 62 | - Monolith-first Strategy: If possible, start with a monolith and design it 63 | with clear [bounded 64 | contexts](https://martinfowler.com/bliki/BoundedContext.html). Then, when 65 | deemed necessary, gradually peel off microservices at the edges. 66 | 67 | After mentioning the disadvantages and dangers of implementing microservices, 68 | why should someone consider using them? 69 | 70 | [@Newman2015] suggests seven key benefits of using microservices: 71 | 72 | - **Technology Heterogeneity**. With a system composed of multiple 73 | collaborating services, we can decide to use different 74 | technologies/programming-languages inside each one. This allows us to pick 75 | the right tool for each job rather than to select a more standardised, 76 | one-size-fits-all approach that often ends up being the lowest common 77 | denominator. 78 | 79 | ```{r, echo = FALSE, out.width = "25%"} 80 | knitr::include_graphics("https://i.imgur.com/vX1u9Po.png") 81 | ``` 82 | 83 | - **Resilience**. A key concept in resilience engineering is the 84 | [bulkhead](https://en.wikipedia.org/wiki/Bulkhead_(partition)) which 85 | originates in ship design (see illustration). If one component of a system 86 | fails, but that failure doesn't cascade, you can isolate the problem and the 87 | rest of the system can carry on working. 88 | 89 | ```{r, echo = FALSE, out.width = "25%"} 90 | knitr::include_graphics("https://i.imgur.com/qelkZ9P.png") 91 | ``` 92 | 93 | - **Scalability**. With a large, monolithic service, we have to scale 94 | everything together. One small part of our overall system is constrained in 95 | performance, but if that behaviour is locked up in a giant monolithic 96 | application, we have to handle scaling everything as a piece. With smaller 97 | services, we can scale those services that need scaling, allowing us to run 98 | other parts of the system on smaller, less powerful hardware. 99 | 100 | ```{r, echo = FALSE, out.width = "25%"} 101 | knitr::include_graphics("https://i.imgur.com/1Tf9Hrh.png") 102 | ``` 103 | 104 | - **Ease of Deployment**. With microservices, we can make a change to a single 105 | service and deploy it independently of the rest of the system. This allows 106 | us to get our code deployed faster. If a problem does occur, it can be 107 | isolated quickly to an individual service, making fast rollback easy to 108 | achieve. It also means we can get our new functionality out to customers 109 | faster. 110 | 111 | ```{r, echo = FALSE, out.width = "25%"} 112 | knitr::include_graphics("https://i.imgur.com/U60xp1V.png") 113 | ``` 114 | 115 | - **Organizational Alignment**. Microservices allow us to better align our 116 | architecture to our organisation, helping us minimise the number of people 117 | working on anyone codebase to hit the sweet spot of team size and 118 | productivity. 119 | 120 | ```{r, echo = FALSE, out.width = "25%"} 121 | knitr::include_graphics("https://i.imgur.com/8mk0BlZ.png") 122 | ``` 123 | 124 | - [***Composability***](https://en.wikipedia.org/wiki/Composability). 125 | Similarly, to the tidyverse packages, where different compositions of 126 | packages are used in different analytic projects in R, with microservices, 127 | we allow for our functionality to be consumed in different ways for 128 | different purposes. For example, a *demand forecasting* service can be 129 | consumed by several different dashboards and a logging system. 130 | 131 | ```{r, echo = FALSE, out.width = "25%"} 132 | knitr::include_graphics("https://i.imgur.com/gHMkhtV.png") 133 | ``` 134 | 135 | - ***Optimizing for Replaceability***. With our services being small in size, 136 | the cost to replace them with a better implementation, or even delete them 137 | altogether, is much easier to manage than in a monolithic app. For example, 138 | during the football world cup, a news website may offer football-related 139 | analysis. Rather than making the analysis part of the website codebase, we 140 | can create a microservice to deliver the football analytic service and 141 | remove it when the tournament is over. 142 | 143 | ```{r, echo = FALSE, out.width = "25%"} 144 | knitr::include_graphics("https://i.imgur.com/SNQAINt.png") 145 | ``` 146 | 147 | To conclude, not every application needs to be built as a microservice. In some 148 | cases, such as in a system that is an amalgam of programming languages and 149 | technologies, microservices architecture is advised or even necessary. However, 150 | seldom it is a good choice to start building an application as a microservice. 151 | Instead, a better option is to design a system in a modular way and implement it 152 | as a monolith. If done well, shifting to microservices would be possible with 153 | reasonable refactoring effort. 154 | 155 | ## Installation 156 | 157 | You can install `microservices` by using: 158 | 159 | ```{r, eval=FALSE, echo = TRUE} 160 | install.packages("microservices") 161 | ``` 162 | 163 | ## Further Reading 164 | 165 | - [YouTube Video](https://www.youtube.com/watch?v=k3PuGGmA7Hg): KPMG case 166 | study for implementing microservices in R (with 167 | [`plumber`](https://www.rplumber.io/) and [RStudio 168 | Connect](https://www.rstudio.com/products/connect/)). 169 | 170 | ## Bibliography 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # `microservices` 3 | 4 | 5 | 6 | [![CRAN 7 | status](https://www.r-pkg.org/badges/version/microservices)](https://CRAN.R-project.org/package=microservices) 8 | [![R build 9 | status](https://github.com/tidylab/microservices/workflows/R-CMD-check/badge.svg)](https://github.com/tidylab/microservices/actions) 10 | [![codecov](https://codecov.io/gh/tidylab/microservices/branch/master/graph/badge.svg?token=ZLBYE2NIWF)](https://app.codecov.io/gh/tidylab/microservices) 11 | 12 | 13 | 14 | Breakdown a Monolithic Application to a Suite of Services 15 | 16 | 17 | 18 | ## Introduction 19 | 20 | ‘Microservice’ architectural style is an approach to developing a single 21 | application as a suite of small services, each running in its own 22 | process and communicating with lightweight mechanisms, often an ‘HTTP’ 23 | resource ‘API’. These services are built around business capabilities 24 | and independently deployable by fully automated deployment machinery. 25 | There is a bare minimum of centralized management of these services, 26 | which may be written in different programming languages and use 27 | different data storage technologies. 28 | 29 | ## Should I use microservices? 30 | 31 | As a start, ask yourself if a microservice architecture is a good choice 32 | for the system you’re working on? 33 | 34 |
35 | 36 | **Caution**: If you do not plan to deploy a system into production, then 37 | you do not need `microservices`. 38 | 39 |
40 | 41 | The microservice architecture entails costs and provides benefits. 42 | Making lots of independent parts work together incurs complexities. 43 | Management, maintenance, support, and testing costs add up in the 44 | system. Many software development efforts would be better off if they 45 | don’t use it. 46 | 47 | If you plan to deploy a system into production, then consider the 48 | following: 49 | 50 | - Favour a monolith over microservices for simple applications. Monolith 51 | can get you to market quicker than microservices. 52 | - Monolith-first Strategy: If possible, start with a monolith and design 53 | it with clear [bounded 54 | contexts](https://martinfowler.com/bliki/BoundedContext.html). Then, 55 | when deemed necessary, gradually peel off microservices at the edges. 56 | 57 | After mentioning the disadvantages and dangers of implementing 58 | microservices, why should someone consider using them? 59 | 60 | ([Newman 2015](#ref-Newman2015)) suggests seven key benefits of using 61 | microservices: 62 | 63 | - **Technology Heterogeneity**. With a system composed of multiple 64 | collaborating services, we can decide to use different 65 | technologies/programming-languages inside each one. This allows us to 66 | pick the right tool for each job rather than to select a more 67 | standardised, one-size-fits-all approach that often ends up being the 68 | lowest common denominator. 69 | 70 | 71 | 72 | - **Resilience**. A key concept in resilience engineering is the 73 | [bulkhead](https://en.wikipedia.org/wiki/Bulkhead_(partition)) which 74 | originates in ship design (see illustration). If one component of a 75 | system fails, but that failure doesn’t cascade, you can isolate the 76 | problem and the rest of the system can carry on working. 77 | 78 | 79 | 80 | - **Scalability**. With a large, monolithic service, we have to scale 81 | everything together. One small part of our overall system is 82 | constrained in performance, but if that behaviour is locked up in a 83 | giant monolithic application, we have to handle scaling everything as 84 | a piece. With smaller services, we can scale those services that need 85 | scaling, allowing us to run other parts of the system on smaller, less 86 | powerful hardware. 87 | 88 | 89 | 90 | - **Ease of Deployment**. With microservices, we can make a change to a 91 | single service and deploy it independently of the rest of the system. 92 | This allows us to get our code deployed faster. If a problem does 93 | occur, it can be isolated quickly to an individual service, making 94 | fast rollback easy to achieve. It also means we can get our new 95 | functionality out to customers faster. 96 | 97 | 98 | 99 | - **Organizational Alignment**. Microservices allow us to better align 100 | our architecture to our organisation, helping us minimise the number 101 | of people working on anyone codebase to hit the sweet spot of team 102 | size and productivity. 103 | 104 | 105 | 106 | - [***Composability***](https://en.wikipedia.org/wiki/Composability). 107 | Similarly, to the tidyverse packages, where different compositions of 108 | packages are used in different analytic projects in R, with 109 | microservices, we allow for our functionality to be consumed in 110 | different ways for different purposes. For example, a *demand 111 | forecasting* service can be consumed by several different dashboards 112 | and a logging system. 113 | 114 | 115 | 116 | - ***Optimizing for Replaceability***. With our services being small in 117 | size, the cost to replace them with a better implementation, or even 118 | delete them altogether, is much easier to manage than in a monolithic 119 | app. For example, during the football world cup, a news website may 120 | offer football-related analysis. Rather than making the analysis part 121 | of the website codebase, we can create a microservice to deliver the 122 | football analytic service and remove it when the tournament is over. 123 | 124 | 125 | 126 | To conclude, not every application needs to be built as a microservice. 127 | In some cases, such as in a system that is an amalgam of programming 128 | languages and technologies, microservices architecture is advised or 129 | even necessary. However, seldom it is a good choice to start building an 130 | application as a microservice. Instead, a better option is to design a 131 | system in a modular way and implement it as a monolith. If done well, 132 | shifting to microservices would be possible with reasonable refactoring 133 | effort. 134 | 135 | ## Installation 136 | 137 | You can install `microservices` by using: 138 | 139 | ``` r 140 | install.packages("microservices") 141 | ``` 142 | 143 | ## Further Reading 144 | 145 | - [YouTube Video](https://www.youtube.com/watch?v=k3PuGGmA7Hg): KPMG 146 | case study for implementing microservices in R (with 147 | [`plumber`](https://www.rplumber.io/) and [RStudio 148 | Connect](https://www.rstudio.com/products/connect/)). 149 | 150 | ## Bibliography 151 | 152 |
153 | 154 |
155 | 156 | Fowler, Martin, and James Lewis. 2014. “Microservices - a definition of this new architectural 158 | term.” . 159 | 160 |
161 | 162 |
163 | 164 | Newman, Sam. 2015. *Building microservices: 165 | designing fine-grained systems*. O’Reilly Media, Inc. 166 | 167 |
168 | 169 |
170 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: 75% 8 | threshold: 75% 9 | patch: off 10 | # default: 11 | # target: auto 12 | # threshold: 1% 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ###################################################### 3 | # R Testing 4 | ###################################################### 5 | r-test: 6 | image: r-package/microservices 7 | build: 8 | context: ./ 9 | dockerfile: ./.dev/docker/r-test/Dockerfile 10 | entrypoint: '/bin/bash' 11 | container_name: r_test 12 | restart: "no" 13 | ###################################################### 14 | # R Plumber 15 | ###################################################### 16 | r-plumber: 17 | image: microservice/plumber:1.0.0 18 | build: 19 | context: ./ 20 | dockerfile: ./.dev/docker/r-plumber/Dockerfile 21 | args: 22 | - R_REPOS='https://mran.microsoft.com/snapshot/2021-08-10' 23 | container_name: r_plumber 24 | restart: always 25 | stdin_open: true 26 | tty: true 27 | ports: 28 | - 8080:8080 29 | version: "3.3" 30 | # networks: 31 | # default: 32 | # name: r-package 33 | -------------------------------------------------------------------------------- /inst/CITATION: -------------------------------------------------------------------------------- 1 | bibentry("Manual", 2 | title = "Building Microservices in R", 3 | author = person("Harel", "Lustiger"), 4 | organization = "Tidylab", 5 | address = "Auckland, New Zealand", 6 | year = version$year, 7 | url = "https://github.com/tidylab/microservices", 8 | 9 | textVersion = 10 | paste("Harel Lustiger (", version$year, "). ", 11 | "Building Microservices in R. ", 12 | "Tidylab, Auckland, New Zealand. ", 13 | "URL https://github.com/tidylab/microservices.", 14 | sep=""), 15 | 16 | mheader = "To cite R in publications use:", 17 | 18 | mfooter = 19 | paste("We have invested a lot of time and effort in creating the microservices package,", 20 | "please cite it when using it for data analysis.", 21 | "See also", sQuote("citation(\"pkgname\")"), 22 | "for citing R packages.", sep = " ") 23 | ) 24 | -------------------------------------------------------------------------------- /inst/REFERENCES.bib: -------------------------------------------------------------------------------- 1 | @misc{Fowler2014, 2 | abstract = {a definition of this new architectural term}, 3 | author = {Fowler, Martin and Lewis, James}, 4 | keywords = {microservice}, 5 | mendeley-tags = {microservice}, 6 | title = {{Microservices - a definition of this new architectural term}}, 7 | url = {https://martinfowler.com/articles/microservices.html}, 8 | year = {2014} 9 | } 10 | @book{Newman2015, 11 | author = {Newman, Sam}, 12 | keywords = {microservice}, 13 | mendeley-tags = {microservice}, 14 | publisher = {O'Reilly Media, Inc.}, 15 | title = {{Building microservices: designing fine-grained systems}}, 16 | year = {2015} 17 | } 18 | @book{RonnieMitra2020, 19 | author = {{Ronnie Mitra} and {Irakli Nadareishvili}}, 20 | isbn = {9781492075455}, 21 | keywords = {microservice}, 22 | mendeley-tags = {microservice}, 23 | publisher = {O'Reilly Media, Inc.}, 24 | title = {{Microservices: Up and Running}}, 25 | year = {2020} 26 | } 27 | @book{Fowler2016, 28 | author = {Fowler, Susan J.}, 29 | isbn = {9781491965979}, 30 | keywords = {microservice}, 31 | mendeley-tags = {microservice}, 32 | publisher = {O'Reilly Media, Inc.}, 33 | title = {{Production-Ready Microservices}}, 34 | year = {2016} 35 | } 36 | -------------------------------------------------------------------------------- /inst/configurations/fs.yml: -------------------------------------------------------------------------------- 1 | default: 2 | add_service: 3 | files: 4 | add: 5 | - tests/testthat/test-endpoint-plumber-{route_name}.R 6 | - inst/endpoints/plumber-{route_name}.R 7 | update: 8 | - inst/entrypoints/plumber-foreground.R 9 | use_microservice: 10 | files: 11 | add: 12 | - tests/testthat/test-endpoint-plumber-utility.R 13 | - inst/configurations/plumber.yml 14 | - inst/endpoints/plumber-utility.R 15 | - inst/entrypoints/plumber-background.R 16 | - inst/entrypoints/plumber-foreground.R 17 | update: 18 | - tests/testthat/helpers-xyz.R 19 | dependencies: 20 | type: 21 | - Suggests 22 | - Suggests 23 | - Suggests 24 | - Imports 25 | - Suggests 26 | - Suggests 27 | - Imports 28 | - Suggests 29 | - Suggests 30 | - Suggests 31 | - Suggests 32 | package: 33 | - config 34 | - httptest 35 | - httr 36 | - jsonlite 37 | - pkgload 38 | - plumber 39 | - purrr 40 | - testthat 41 | - usethis 42 | - promises 43 | - future 44 | version: 45 | - '*' 46 | - '*' 47 | - '*' 48 | - '*' 49 | - '*' 50 | - '>= 1.0.0' 51 | - '*' 52 | - '*' 53 | - '*' 54 | - '*' 55 | - '*' 56 | -------------------------------------------------------------------------------- /inst/configurations/openapi.yml: -------------------------------------------------------------------------------- 1 | # https://swagger.io/docs/specification/about/ 2 | openapi: 3.0.1 3 | info: 4 | title: RestRserve OpenAPI 5 | version: '1.0' 6 | servers: 7 | - url: / 8 | paths: 9 | /fib: 10 | get: 11 | description: Calculates Fibonacci number 12 | parameters: 13 | - name: "n" 14 | description: >- 15 | x for Fibonnacci number. Example:
16 | n = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
17 | x = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... 18 | in: query 19 | schema: 20 | type: integer 21 | example: 10 22 | required: true 23 | responses: 24 | 200: 25 | description: API response 26 | content: 27 | text/plain: 28 | schema: 29 | type: string 30 | example: 5 31 | 400: 32 | description: Bad Request 33 | -------------------------------------------------------------------------------- /inst/configurations/plumber.yml: -------------------------------------------------------------------------------- 1 | default: 2 | scheme: http 3 | host: 127.0.0.1 4 | port: 8080 5 | -------------------------------------------------------------------------------- /inst/endpoints/plumber-utility.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber utility endpoint 3 | ################################################################################ 4 | # Utilities --------------------------------------------------------------- 5 | #* Health check 6 | #* Respond when you ask it if a service is available. 7 | #* @serializer unboxedJSON list(na = NULL) 8 | #* @get healthcheck 9 | function(){ 10 | message("--> healthcheck: Request Received") 11 | return(NULL) 12 | } 13 | 14 | #* Reflect the input class 15 | #* Return the class of the input. 16 | #* @serializer unboxedJSON list(na = NULL) 17 | #* @post class 18 | function(req){ 19 | message("--> class: Request Received") 20 | json <- req$postBody 21 | x <- json |> jsonlite::fromJSON(flatten = TRUE) 22 | return(class(x)) 23 | } 24 | -------------------------------------------------------------------------------- /inst/endpoints/plumber-{route_name}.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber route_name endpoint 3 | ################################################################################ 4 | #' @title Establish Database 5 | #' @description Load the data tables within the datasets package into an environment 6 | #' @note Global code gets executed at plumb() time. 7 | database <- new.env() 8 | names <- utils::data(package = "datasets")$results[, "Item"] 9 | sapply(names, function(x) utils::data(list = x, package = "datasets", envir = database)) 10 | 11 | 12 | # Core Functions ---------------------------------------------------------- 13 | list_tables <- function(envir) ls(envir) 14 | read_table <- function(envir, name) as.data.frame(envir[[name]]) 15 | write_table <- function(envir, name, value) assign(name, value, envir = envir) 16 | 17 | 18 | # list_tables ------------------------------------------------------------- 19 | #* List remote tables 20 | #* Returns the names of remote tables accessible through this connection. 21 | #* @serializer unboxedJSON list(na = NULL) 22 | #* @get list_tables 23 | function(){ 24 | return(list_tables(database)) 25 | } 26 | 27 | 28 | # read_table -------------------------------------------------------------- 29 | #* Copy a data frame from database tables 30 | #* Returns the database table as a data frame 31 | #* @param name (`character`) What is the name of the data table? 32 | #* @param nrow (`integer`) How many rows should be returned? 33 | #* @serializer unboxedJSON list(na = NULL) 34 | #* @get read_table 35 | #* @post read_table 36 | function(res, name, nrow){ 37 | if(missing(nrow)) nrow <- Inf 38 | 39 | tryCatch({ 40 | name <- match.arg(name, list_tables(database)) 41 | tail(read_table(database, name), n = nrow) 42 | }, 43 | error = function(e){ 44 | res$status <- 400 45 | return(list(error = jsonlite::unbox(e$message))) 46 | }) 47 | } 48 | 49 | 50 | # write_table ------------------------------------------------------------- 51 | #* Copy a data frame from database tables 52 | #* Writes or overwrites a data frame to a database table 53 | #* @serializer unboxedJSON list(na = NULL) 54 | #* @post write_table 55 | function(req){ 56 | try(write_table(database, req$args$name, req$args$value)) 57 | return(NULL) 58 | } 59 | -------------------------------------------------------------------------------- /inst/entrypoints/plumber-background.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber: Quick Start Guide 3 | ## 4 | ################################################################################ 5 | path <- usethis::proj_path("inst", 'entrypoints', 'plumber-foreground.R') 6 | rstudioapi::jobRunScript( 7 | path = path, 8 | name = "Plumber API", 9 | workingDir = ".", 10 | importEnv = FALSE, 11 | exportEnv = "" 12 | ) 13 | -------------------------------------------------------------------------------- /inst/entrypoints/plumber-foreground.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber: Quick Start Guide 3 | ## 4 | ################################################################################ 5 | usethis::proj_set() 6 | if(!is.null(pkgload::pkg_ns())) pkgload::load_code() else pkgload::load_all() 7 | 8 | plan <- purrr::partial(future::plan, workers = future::availableCores()) 9 | if(future::supportsMulticore()) plan(future::multicore) else plan(future::multisession) 10 | 11 | endpoint_path <- usethis::proj_path('inst', 'endpoints') 12 | config <- config::get(file = usethis::proj_path('inst', 'configurations', 'plumber.yml')) 13 | 14 | root <- plumber::pr() 15 | root$mount('utility', plumber::Plumber$new(file.path(endpoint_path, 'plumber-utility.R'))) 16 | root$mount('route_name', plumber::Plumber$new(file.path(endpoint_path, 'plumber-{route_name}.R'))) 17 | 18 | root$setDocsCallback(NULL) 19 | root$run(host = config$host, port = config$port) 20 | -------------------------------------------------------------------------------- /inst/snippets/demo-RestRserve-package.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## RestRserve: Quick Start Guide 3 | ## 4 | ################################################################################ 5 | 6 | # Step 1: Create application ---------------------------------------------- 7 | app <- RestRserve::Application$new() 8 | 9 | # Step 2: Define logic ---------------------------------------------------- 10 | #' 2.1 Create a function: Fibonacci Numbers 11 | #' n = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ... 12 | #' x = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... 13 | calc_fib <- function(n) { 14 | if (n < 0L) stop("n should be >= 0") 15 | if (n == 0L) return(0L) 16 | if (n == 1L || n == 2L) return(1L) 17 | x = rep(1L, n) 18 | 19 | for (i in 3L:n) { 20 | x[[i]] = x[[i - 1]] + x[[i - 2]] 21 | } 22 | 23 | return(x[[n]]) 24 | } 25 | 26 | ## 2.2 Create a handler which will handle requests. 27 | fib_handler = function(request, response) { 28 | n = as.integer(request$parameters_query[["n"]]) 29 | if (length(n) == 0L || is.na(n)) { 30 | RestRserve::raise(RestRserve::raiseHTTPError$bad_request()) 31 | } 32 | response$set_body(as.character(calc_fib(n))) 33 | response$set_content_type("text/plain") 34 | } 35 | 36 | 37 | # Step 3: Register endpoint ----------------------------------------------- 38 | app$add_get(path = "/fib", FUN = fib_handler) 39 | 40 | 41 | # Step 4: Test endpoints -------------------------------------------------- 42 | request <- RestRserve::Request$new(path = "/fib", parameters_query = list(n = 10)) 43 | response <- app$process_request(request) 44 | 45 | cat("Response status:", response$status) 46 | #> Response status: 200 OK 47 | cat("Response body:", response$body) 48 | #> Response body: 55 49 | 50 | 51 | # Step 5: Add OpenAPI description and Swagger UI -------------------------- 52 | yaml_file <- usethis::proj_path("inst", "config", "openapi.yml") 53 | app$add_openapi(path = "/openapi.yaml", file_path = yaml_file) 54 | app$add_swagger_ui(path = "/doc", path_openapi = "/openapi.yaml", use_cdn = TRUE) 55 | 56 | 57 | # Step 6: Start the app --------------------------------------------------- 58 | backend <- RestRserve::BackendRserve$new() 59 | # backend$start(app, http_port = 8080) 60 | 61 | path <- tempfile(pattern = "RestRserve-", fileext = ".R") 62 | writeLines("backend$start(app, http_port = 8080)", path) 63 | 64 | try(rstudioapi::jobSetState(Sys.getenv("RSTUDIO_JOB_ID"), "cancelled"), silent = TRUE) 65 | RSTUDIO_JOB_ID <- rstudioapi::jobRunScript( 66 | path = path, 67 | name = "RestRserve App", workingDir = ".", importEnv = TRUE, 68 | exportEnv = "" 69 | ) 70 | Sys.setenv(RSTUDIO_JOB_ID = RSTUDIO_JOB_ID) 71 | 72 | # Step 7: Check it works -------------------------------------------------- 73 | browseURL("http://localhost:8080/doc") 74 | browseURL("http://localhost:8080/fib?n=10") 75 | 76 | -------------------------------------------------------------------------------- /inst/snippets/enable-docker-at-startup.R: -------------------------------------------------------------------------------- 1 | # Reference: 2 | # 3 | 4 | # 1. Create docker_boot.service 5 | 6 | 7 | # 2. Copy docker_boot.service to systemd 8 | system("sudo cd ~") 9 | system("sudo cp -v ./microservices/docker_boot.service /etc/systemd/system") 10 | 11 | 12 | # 3. Enable and start docker_boot.service 13 | system("sudo docker-compose down") 14 | # system("sudo systemctl unmask docker") 15 | system("sudo systemctl enable docker") 16 | system("sudo systemctl start docker") 17 | 18 | # system("sudo systemctl unmask docker_boot.service") 19 | system("sudo systemctl enable docker_boot.service") 20 | system("sudo systemctl start docker_boot.service") 21 | 22 | 23 | # 4. Check status of the docker_boot.service 24 | system("sudo systemctl status docker_boot.service") 25 | 26 | 27 | # 5. Check if the microservice is up 28 | system("sudo curl -L localhost:8080/__docs__/") 29 | -------------------------------------------------------------------------------- /inst/templates/R/globals.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | ## Required for {tidyverse} 3 | utils::globalVariables(".") 4 | 5 | -------------------------------------------------------------------------------- /inst/templates/R/plumber-add_service.R: -------------------------------------------------------------------------------- 1 | #' @title Add a Service Route to the Microservice 2 | #' @description Expose additional set of services on a separate URL. 3 | #' 4 | #' @inheritParams use_microservice 5 | #' @param name (`character`) what is the service route name? For example, if 6 | #' \code{name} = "repository" then the set of services would become available 7 | #' at `http://127.0.0.1:8080/repository/`. 8 | #' 9 | #' @details 10 | #' ```{r child = "vignettes/details/add_service.Rmd"} 11 | #' ```` 12 | #' @return No return value, called for side effects. 13 | #' @family plumber microservice 14 | #' @export 15 | #' @examples 16 | #' path <- tempfile() 17 | #' dir.create(path, showWarnings = FALSE, recursive = TRUE) 18 | #' use_microservice(path) 19 | #' 20 | #' add_service(path, name = "repository") 21 | #' 22 | #' list.files(path, recursive = TRUE) 23 | add_service <- function(path = ".", name, overwrite = FALSE){ 24 | name <- gsub(" |\\.", "-", tolower(name)) 25 | .add_service$assert_microservice_exists(path) 26 | .add_service$mount_service(path, name) 27 | .add_service$endpoint_test(path, name, overwrite = overwrite) 28 | .add_service$endpoint_script(path, name, overwrite = overwrite) 29 | 30 | invisible() 31 | } 32 | 33 | 34 | # low-level functions ----------------------------------------------------- 35 | .add_service <- new.env() 36 | 37 | .add_service$assert_microservice_exists <- function(path){ 38 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 39 | files <- config::get("use_microservice", file = file_fs)$files$add 40 | 41 | missing_files <- names(Filter(isFALSE, sapply(file.path(path, files), file.exists))) 42 | if(length(missing_files) == 0) return(invisible()) 43 | stop("\nDid you call use_microservice()? Couldn't find:\n", paste("-->", missing_files, collapse = "\n")) 44 | } 45 | 46 | .add_service$mount_service <- function(path, name){ 47 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 48 | files <- config::get("use_microservice", file = file_fs)$files$add 49 | file <- file.path(path ,files[grepl("plumber-foreground.R", files)]) 50 | 51 | content <- readLines(file) 52 | content <- content[!grepl("route_name", content)] 53 | 54 | row_index <- which.max(grepl("root\\$mount", content)) 55 | new_row <- paste0("root$mount('", name, "', plumber::Plumber$new(file.path(endpoint_path, 'plumber-", name, ".R')))") 56 | 57 | content <- append(content, new_row, row_index) 58 | writeLines(content, file) 59 | invisible() 60 | } 61 | 62 | .add_service$endpoint_test <- function(path, name, overwrite){ 63 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 64 | files <- config::get("add_service", file = file_fs)$files$add 65 | file <- files[grepl("test-", files)] 66 | root <- system.file("templates", package = "microservices", mustWork = TRUE) 67 | 68 | source_file <- file.path(root, file) 69 | content <- readLines(source_file) 70 | content <- gsub("route_name", name, content) 71 | 72 | target_file <- file.path(path, glue::glue(file, route_name = name)) 73 | if(file.exists(target_file) & isFALSE(overwrite)) return() 74 | 75 | dir.create(dirname(target_file), F, T); file.create(target_file) 76 | writeLines(content, target_file) 77 | 78 | invisible() 79 | } 80 | 81 | .add_service$endpoint_script <- function(path, name, overwrite){ 82 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 83 | files <- config::get("add_service", file = file_fs)$files$add 84 | file <- files[grepl("/endpoints/", files)] 85 | root <- system.file("templates", package = "microservices", mustWork = TRUE) 86 | 87 | source_file <- file.path(root, file) 88 | content <- readLines(source_file) 89 | content <- gsub("route_name", name, content) 90 | 91 | target_file <- file.path(path, glue::glue(file, route_name = name)) 92 | if(file.exists(target_file) & isFALSE(overwrite)) return() 93 | 94 | dir.create(dirname(target_file), F, T); file.create(target_file) 95 | writeLines(content, target_file) 96 | 97 | invisible() 98 | } 99 | -------------------------------------------------------------------------------- /inst/templates/R/plumber-use_microservice.R: -------------------------------------------------------------------------------- 1 | #' @title Use a plumber Microservice in an R Project 2 | #' @description 3 | #' Lay the infrastructure for a microservice. That includes unit test, 4 | #' dependency packages, configuration file, entrypoints and utility endpoint. 5 | #' 6 | #' @param path (`character`) Where is the project root folder? 7 | #' @param overwrite (`logical`) Should existing destination files be overwritten? 8 | #' 9 | #' @details 10 | #' ```{r child = "vignettes/details/use_microservice.Rmd"} 11 | #' ```` 12 | #' 13 | #' @return No return value, called for side effects. 14 | #' @family plumber microservice 15 | #' @export 16 | #' @examples 17 | #' path <- tempfile() 18 | #' use_microservice(path) 19 | #' 20 | #' list.files(path, recursive = TRUE) 21 | #' 22 | #' cat(read.dcf(file.path(path, "DESCRIPTION"), "Imports")) 23 | #' cat(read.dcf(file.path(path, "DESCRIPTION"), "Suggests")) 24 | use_microservice <- function(path = ".", overwrite = FALSE){ 25 | dir.create(path, FALSE, TRUE) 26 | .use_microservice$add_files(path = path, overwrite = overwrite) 27 | .use_microservice$update_files(path = path) 28 | .use_microservice$add_dependencies(path = path) 29 | invisible() 30 | } 31 | 32 | 33 | # low-level functions ----------------------------------------------------- 34 | .use_microservice <- new.env() 35 | 36 | .use_microservice$add_files <- function(path, overwrite){ 37 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 38 | files <- config::get("use_microservice", file = file_fs)$files$add 39 | 40 | for(file in files){ 41 | file_source <- fs::path_package("microservices", "templates", gsub("plumber-utility\\.R$", "plumber-{route_name}.R", file)) 42 | file_target <- file.path(path, file) 43 | dir.create(dirname(file_target), showWarnings = FALSE, recursive = TRUE) 44 | file.copy(from = file_source, to = file_target, overwrite = overwrite) 45 | } 46 | } 47 | 48 | .use_microservice$update_files <- function(path, overwrite){ 49 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 50 | files <- config::get("use_microservice", file = file_fs)$files$update 51 | 52 | for(file in files){ 53 | file_source <- system.file(package = "microservices", "templates", file, mustWork = TRUE) 54 | file_target <- file.path(path, file) 55 | dir.create(dirname(file_target), showWarnings = FALSE, recursive = TRUE) 56 | if(file.does.not.exist(file_target)) file.create(file_target) 57 | content <- readLines(file_source) 58 | write(content, file_target, append = TRUE) 59 | } 60 | 61 | invisible() 62 | } 63 | 64 | .use_microservice$add_dependencies <- function(path){ 65 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 66 | dependencies <- config::get("use_microservice", file = file_fs)$dependencies |> as.data.frame() 67 | 68 | desc <- .utils$get_description_obj(path = path) 69 | desc$set_deps(dependencies)$write(file.path(path, "DESCRIPTION")) 70 | } 71 | 72 | -------------------------------------------------------------------------------- /inst/templates/R/utils-DockerCompose.R: -------------------------------------------------------------------------------- 1 | # DockerCompose ----------------------------------------------------------- 2 | #' @title Use a docker-compose.yml File 3 | #' @description 4 | #' GIVEN a \code{docker-compose.yml}, 5 | #' WEHN \code{DockerCompose} is instantiated, 6 | #' THEN the resulting object gives access to Docker commands. 7 | #' @param service (`character`) Service name in \code{docker-compose.yml}. 8 | #' @param field (`character`) Field name in \code{docker-compose.yml}. 9 | #' @param slug (`character`) URL slug (e.g. \code{shiny-app-name}). 10 | #' @family docker 11 | #' @export 12 | DockerCompose <- R6::R6Class(# nocov start 13 | classname = "DockerCompose", 14 | cloneable = FALSE, 15 | lock_objects = FALSE, 16 | public = list( 17 | # Public Methods ------------------------------------------------------- 18 | #' @description 19 | #' Initialize a DockerCompose object 20 | #' @param path_docker_compose (`character`) Path to docker-compose file. 21 | initialize = function(path_docker_compose = "./docker-compose.yml"){ 22 | stopifnot(file.exists(path_docker_compose)) 23 | private$path_docker_compose <- path_docker_compose 24 | private$composition <- yaml::read_yaml(path_docker_compose, eval.expr = FALSE) 25 | invisible(self) 26 | }, 27 | #' @description 28 | #' Get a value from a service 29 | #' @examples \donttest{\dontrun{DockerCompose$new()$get("shinyserver", "ports")}} 30 | get = function(service, field) DockerCompose$funs$get(self, private, service, field), 31 | #' @description 32 | #' Create and start containers. 33 | start = function(service = NULL) DockerCompose$funs$start(self, private, service), 34 | #' @description 35 | #' Stop containers. 36 | stop = function() DockerCompose$funs$stop(self, private), 37 | #' @description 38 | #' Restart containers. 39 | restart = function(service = NULL) DockerCompose$funs$restart(self, private, service), 40 | #' @description 41 | #' Stop and remove containers, networks, images and volumes. 42 | reset = function() DockerCompose$funs$reset(self, private), 43 | #' @description 44 | #' Load URL into an HTML Browser 45 | browse_url = function(service, slug = "") DockerCompose$funs$browse_url(self, private, service, slug) 46 | ),# end public 47 | private = list( 48 | path_docker_compose = c(), 49 | composition = list() 50 | ) 51 | )# nocov end 52 | DockerCompose$funs <- new.env() 53 | 54 | # Public Methods ---------------------------------------------------------- 55 | DockerCompose$funs$reset <- function(self, private){ 56 | system <- DockerCompose$funs$system 57 | docker_commands <- c( 58 | "docker-compose down", 59 | "docker system prune -f", 60 | "docker volume prune -f", 61 | "docker network prune -f", 62 | "docker rmi -f $(docker images -a -q)" 63 | ) 64 | sapply(docker_commands, function(x) try(system(x, wait = TRUE))) 65 | invisible(self) 66 | } 67 | 68 | DockerCompose$funs$restart <- function(self, private, service){ 69 | system <- DockerCompose$funs$system 70 | DockerCompose$funs$stop(self, private) 71 | DockerCompose$funs$start(self, private, service) 72 | invisible(self) 73 | } 74 | 75 | DockerCompose$funs$start <- function(self, private, service){ 76 | is.not.null <- Negate(is.null) 77 | if(is.not.null(service)){ 78 | service <- match.arg(service, names(private$composition$services), several.ok = TRUE) 79 | } 80 | 81 | system <- DockerCompose$funs$system 82 | docker_command <- glue::glue("docker-compose up -d --build {services}", services = paste0(service, collapse = " ")) 83 | system(docker_command, wait = TRUE) 84 | invisible(self) 85 | } 86 | 87 | DockerCompose$funs$stop <- function(self, private){ 88 | system <- DockerCompose$funs$system 89 | docker_command <- glue::glue("docker-compose down") 90 | system(docker_command, wait = TRUE) 91 | invisible(self) 92 | } 93 | 94 | DockerCompose$funs$browse_url <- function(self, private, service, slug){ 95 | service <- match.arg(service, names(private$composition$services)) 96 | url <- "localhost" 97 | port <- stringr::str_remove(self$get(service, "ports"), ":.*") 98 | if(length(port) == 0) port <- "8080" 99 | address <- glue::glue("http://{url}:{port}/{slug}", url = "localhost", port = port, slug = slug) 100 | try(browseURL(utils::URLencode(address))) 101 | return(self) 102 | } 103 | 104 | DockerCompose$funs$get <- function(self, private, service, field){ 105 | stopifnot(!missing(field)) 106 | service <- match.arg(service, names(private$composition$services)) 107 | private$composition$services[[service]][[field]] 108 | } 109 | 110 | # Helpers ----------------------------------------------------------------- 111 | DockerCompose$funs$system <- function(command, ...){ message("\033[43m\033[44m",command,"\033[43m\033[49m") ; base::system(command, ...) } 112 | DockerCompose$funs$escape_character <- function(x){ if(is.character(x)) paste0('"', x, '"') else x } 113 | -------------------------------------------------------------------------------- /inst/templates/R/utils.R: -------------------------------------------------------------------------------- 1 | #' @noRd 2 | imports_field <- function() { 3 | config::get 4 | desc::desc 5 | dplyr::filter 6 | fs::path 7 | glue::glue 8 | purrr::walk 9 | withr::with_options 10 | } 11 | 12 | # Negates ----------------------------------------------------------------- 13 | file.does.not.exist <- Negate(base::file.exists) 14 | is.not.null <- Negate(base::is.null) 15 | 16 | # Helpers ----------------------------------------------------------------- 17 | .utils <- new.env() 18 | 19 | .utils$get_description_obj <- function(path){ 20 | desc_file <- file.path(path, "DESCRIPTION") 21 | dir.create(path, showWarnings = FALSE, recursive = TRUE) 22 | if(file.does.not.exist(desc_file)) withr::with_dir(path, desc::description$new("!new")$write(file = "DESCRIPTION")) 23 | return(desc::description$new(desc_file)) 24 | } 25 | -------------------------------------------------------------------------------- /inst/templates/R/zzz.R: -------------------------------------------------------------------------------- 1 | .onAttach <- function(lib, pkg,...){#nocov start 2 | options( 3 | usethis.quiet = TRUE 4 | ) 5 | 6 | if(interactive()) packageStartupMessage( 7 | paste( 8 | "\n\033[44m\033[37m", 9 | "\nWelcome to microservices", 10 | "\nMore information, vignettes, and guides are available on the microservices project website:", 11 | "\nhttps://tidylab.github.io/microservices/", 12 | "\n\033[39m\033[49m", 13 | sep="") 14 | ) 15 | }#nocov end 16 | -------------------------------------------------------------------------------- /inst/templates/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ###################################################### 3 | # R Testing 4 | ###################################################### 5 | r-test: 6 | image: r-package/microservices 7 | build: 8 | context: ./ 9 | dockerfile: ./.dev/docker/r-test/Dockerfile 10 | entrypoint: '/bin/bash' 11 | container_name: r_test 12 | restart: "no" 13 | ###################################################### 14 | # R Plumber 15 | ###################################################### 16 | r-plumber: 17 | image: microservice/plumber:1.0.0 18 | build: 19 | context: ./ 20 | dockerfile: ./.dev/docker/r-plumber/Dockerfile 21 | args: 22 | - R_REPOS='https://mran.microsoft.com/snapshot/2021-08-10' 23 | container_name: r_plumber 24 | restart: always 25 | stdin_open: true 26 | tty: true 27 | ports: 28 | - 8080:8080 29 | version: "3.3" 30 | # networks: 31 | # default: 32 | # name: r-package 33 | -------------------------------------------------------------------------------- /inst/templates/inst/configurations/fs.yml: -------------------------------------------------------------------------------- 1 | default: 2 | add_service: 3 | files: 4 | add: 5 | - tests/testthat/test-endpoint-plumber-{route_name}.R 6 | - inst/endpoints/plumber-{route_name}.R 7 | update: 8 | - inst/entrypoints/plumber-foreground.R 9 | use_microservice: 10 | files: 11 | add: 12 | - tests/testthat/test-endpoint-plumber-utility.R 13 | - inst/configurations/plumber.yml 14 | - inst/endpoints/plumber-utility.R 15 | - inst/entrypoints/plumber-background.R 16 | - inst/entrypoints/plumber-foreground.R 17 | update: 18 | - tests/testthat/helpers-xyz.R 19 | dependencies: 20 | type: 21 | - Suggests 22 | - Suggests 23 | - Suggests 24 | - Imports 25 | - Suggests 26 | - Suggests 27 | - Imports 28 | - Suggests 29 | - Suggests 30 | - Suggests 31 | - Suggests 32 | package: 33 | - config 34 | - httptest 35 | - httr 36 | - jsonlite 37 | - pkgload 38 | - plumber 39 | - purrr 40 | - testthat 41 | - usethis 42 | - promises 43 | - future 44 | version: 45 | - '*' 46 | - '*' 47 | - '*' 48 | - '*' 49 | - '*' 50 | - '>= 1.0.0' 51 | - '*' 52 | - '*' 53 | - '*' 54 | - '*' 55 | - '*' 56 | -------------------------------------------------------------------------------- /inst/templates/inst/configurations/openapi.yml: -------------------------------------------------------------------------------- 1 | # https://swagger.io/docs/specification/about/ 2 | openapi: 3.0.1 3 | info: 4 | title: RestRserve OpenAPI 5 | version: '1.0' 6 | servers: 7 | - url: / 8 | paths: 9 | /fib: 10 | get: 11 | description: Calculates Fibonacci number 12 | parameters: 13 | - name: "n" 14 | description: >- 15 | x for Fibonnacci number. Example:
16 | n = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
17 | x = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... 18 | in: query 19 | schema: 20 | type: integer 21 | example: 10 22 | required: true 23 | responses: 24 | 200: 25 | description: API response 26 | content: 27 | text/plain: 28 | schema: 29 | type: string 30 | example: 5 31 | 400: 32 | description: Bad Request 33 | -------------------------------------------------------------------------------- /inst/templates/inst/configurations/plumber.yml: -------------------------------------------------------------------------------- 1 | default: 2 | scheme: http 3 | host: 127.0.0.1 4 | port: 8080 5 | -------------------------------------------------------------------------------- /inst/templates/inst/endpoints/plumber-utility.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber utility endpoint 3 | ################################################################################ 4 | # Utilities --------------------------------------------------------------- 5 | #* Health check 6 | #* Respond when you ask it if a service is available. 7 | #* @serializer unboxedJSON list(na = NULL) 8 | #* @get healthcheck 9 | function(){ 10 | message("--> healthcheck: Request Received") 11 | return(NULL) 12 | } 13 | 14 | #* Reflect the input class 15 | #* Return the class of the input. 16 | #* @serializer unboxedJSON list(na = NULL) 17 | #* @post class 18 | function(req){ 19 | message("--> class: Request Received") 20 | json <- req$postBody 21 | x <- json |> jsonlite::fromJSON(flatten = TRUE) 22 | return(class(x)) 23 | } 24 | -------------------------------------------------------------------------------- /inst/templates/inst/endpoints/plumber-{route_name}.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber route_name endpoint 3 | ################################################################################ 4 | #' @title Establish Database 5 | #' @description Load the data tables within the datasets package into an environment 6 | #' @note Global code gets executed at plumb() time. 7 | database <- new.env() 8 | names <- utils::data(package = "datasets")$results[, "Item"] 9 | sapply(names, function(x) utils::data(list = x, package = "datasets", envir = database)) 10 | 11 | 12 | # Core Functions ---------------------------------------------------------- 13 | list_tables <- function(envir) ls(envir) 14 | read_table <- function(envir, name) as.data.frame(envir[[name]]) 15 | write_table <- function(envir, name, value) assign(name, value, envir = envir) 16 | 17 | 18 | # list_tables ------------------------------------------------------------- 19 | #* List remote tables 20 | #* Returns the names of remote tables accessible through this connection. 21 | #* @serializer unboxedJSON list(na = NULL) 22 | #* @get list_tables 23 | function(){ 24 | return(list_tables(database)) 25 | } 26 | 27 | 28 | # read_table -------------------------------------------------------------- 29 | #* Copy a data frame from database tables 30 | #* Returns the database table as a data frame 31 | #* @param name (`character`) What is the name of the data table? 32 | #* @param nrow (`integer`) How many rows should be returned? 33 | #* @serializer unboxedJSON list(na = NULL) 34 | #* @get read_table 35 | #* @post read_table 36 | function(res, name, nrow){ 37 | if(missing(nrow)) nrow <- Inf 38 | 39 | tryCatch({ 40 | name <- match.arg(name, list_tables(database)) 41 | tail(read_table(database, name), n = nrow) 42 | }, 43 | error = function(e){ 44 | res$status <- 400 45 | return(list(error = jsonlite::unbox(e$message))) 46 | }) 47 | } 48 | 49 | 50 | # write_table ------------------------------------------------------------- 51 | #* Copy a data frame from database tables 52 | #* Writes or overwrites a data frame to a database table 53 | #* @serializer unboxedJSON list(na = NULL) 54 | #* @post write_table 55 | function(req){ 56 | try(write_table(database, req$args$name, req$args$value)) 57 | return(NULL) 58 | } 59 | -------------------------------------------------------------------------------- /inst/templates/inst/entrypoints/plumber-background.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber: Quick Start Guide 3 | ## 4 | ################################################################################ 5 | path <- usethis::proj_path("inst", 'entrypoints', 'plumber-foreground.R') 6 | rstudioapi::jobRunScript( 7 | path = path, 8 | name = "Plumber API", 9 | workingDir = ".", 10 | importEnv = FALSE, 11 | exportEnv = "" 12 | ) 13 | -------------------------------------------------------------------------------- /inst/templates/inst/entrypoints/plumber-foreground.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## plumber: Quick Start Guide 3 | ## 4 | ################################################################################ 5 | usethis::proj_set() 6 | if(!is.null(pkgload::pkg_ns())) pkgload::load_code() else pkgload::load_all() 7 | 8 | plan <- purrr::partial(future::plan, workers = future::availableCores()) 9 | if(future::supportsMulticore()) plan(future::multicore) else plan(future::multisession) 10 | 11 | endpoint_path <- usethis::proj_path('inst', 'endpoints') 12 | config <- config::get(file = usethis::proj_path('inst', 'configurations', 'plumber.yml')) 13 | 14 | root <- plumber::pr() 15 | root$mount('utility', plumber::Plumber$new(file.path(endpoint_path, 'plumber-utility.R'))) 16 | root$mount('route_name', plumber::Plumber$new(file.path(endpoint_path, 'plumber-{route_name}.R'))) 17 | 18 | root$setDocsCallback(NULL) 19 | root$run(host = config$host, port = config$port) 20 | -------------------------------------------------------------------------------- /inst/templates/tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(microservices) 3 | test_check("microservices") 4 | -------------------------------------------------------------------------------- /inst/templates/tests/testthat/_api/127.0.0.1-8080/route_name/read_table-a26694.R: -------------------------------------------------------------------------------- 1 | structure(list(url = "http://127.0.0.1:8080/route_name/read_table?name=xxx", 2 | status_code = 400L, headers = structure(list(date = "Sat, 23 Oct 2021 00:54:58 GMT", 3 | `content-type` = "application/json", `content-length` = "1358"), class = c("insensitive", 4 | "list")), all_headers = list(list(status = 400L, version = "HTTP/1.1", 5 | headers = structure(list(date = "Sat, 23 Oct 2021 00:54:58 GMT", 6 | `content-type` = "application/json", `content-length` = "1358"), class = c("insensitive", 7 | "list")))), cookies = structure(list(domain = logical(0), 8 | flag = logical(0), path = logical(0), secure = logical(0), 9 | expiration = structure(numeric(0), class = c("POSIXct", 10 | "POSIXt")), name = logical(0), value = logical(0)), row.names = integer(0), class = "data.frame"), 11 | content = charToRaw("{\"error\":\"'arg' should be one of \\\"ability.cov\\\", \\\"airmiles\\\", \\\"AirPassengers\\\", \\\"airquality\\\", \\\"anscombe\\\", \\\"attenu\\\", \\\"attitude\\\", \\\"austres\\\", \\\"BJsales\\\", \\\"BJsales.lead\\\", \\\"BOD\\\", \\\"cars\\\", \\\"ChickWeight\\\", \\\"chickwts\\\", \\\"co2\\\", \\\"CO2\\\", \\\"crimtab\\\", \\\"discoveries\\\", \\\"DNase\\\", \\\"esoph\\\", \\\"euro\\\", \\\"euro.cross\\\", \\\"eurodist\\\", \\\"EuStockMarkets\\\", \\\"faithful\\\", \\\"Formaldehyde\\\", \\\"freeny\\\", \\\"freeny.x\\\", \\\"freeny.y\\\", \\\"HairEyeColor\\\", \\\"Harman23.cor\\\", \\\"Harman74.cor\\\", \\\"Indometh\\\", \\\"infert\\\", \\\"InsectSprays\\\", \\\"iris\\\", \\\"iris3\\\", \\\"islands\\\", \\\"JohnsonJohnson\\\", \\\"LakeHuron\\\", \\\"lh\\\", \\\"LifeCycleSavings\\\", \\\"Loblolly\\\", \\\"longley\\\", \\\"lynx\\\", \\\"morley\\\", \\\"mtcars\\\", \\\"nhtemp\\\", \\\"Nile\\\", \\\"nottem\\\", \\\"npk\\\", \\\"occupationalStatus\\\", \\\"Orange\\\", \\\"OrchardSprays\\\", \\\"PlantGrowth\\\", \\\"precip\\\", \\\"presidents\\\", \\\"pressure\\\", \\\"Puromycin\\\", \\\"quakes\\\", \\\"randu\\\", \\\"rivers\\\", \\\"rock\\\", \\\"Seatbelts\\\", \\\"sleep\\\", \\\"stack.loss\\\", \\\"stack.x\\\", \\\"stackloss\\\", \\\"sunspot.month\\\", \\\"sunspot.year\\\", \\\"sunspots\\\", \\\"swiss\\\", \\\"Theoph\\\", \\\"Titanic\\\", \\\"ToothGrowth\\\", \\\"treering\\\", \\\"trees\\\", \\\"UCBAdmissions\\\", \\\"UKDriverDeaths\\\", \\\"UKgas\\\", \\\"USAccDeaths\\\", \\\"USArrests\\\", \\\"UScitiesD\\\", \\\"USJudgeRatings\\\", \\\"USPersonalExpenditure\\\", \\\"uspop\\\", \\\"VADeaths\\\", \\\"volcano\\\", \\\"warpbreaks\\\", \\\"women\\\", \\\"WorldPhones\\\", \\\"WWWusage\\\", \\\"zzz\\\"\"}"), 12 | date = structure(1634950498, class = c("POSIXct", "POSIXt" 13 | ), tzone = "GMT"), times = c(redirect = 0, namelookup = 4.3e-05, 14 | connect = 4.4e-05, pretransfer = 0.00012, starttransfer = 0.00367, 15 | total = 0.003694)), class = "response") 16 | -------------------------------------------------------------------------------- /inst/templates/tests/testthat/helpers-xyz.R: -------------------------------------------------------------------------------- 1 | # httptest ---------------------------------------------------------------- 2 | options(httptest.debug = FALSE) 3 | 4 | test_http <- function(desc, code){ 5 | withr::local_package("httptest") 6 | old_dir <- httptest::.mockPaths()[1] 7 | on.exit(httptest::.mockPaths(old_dir)) 8 | httptest::.mockPaths("_api") 9 | testthat::test_that(desc, { 10 | suppressWarnings(tryCatch( 11 | suppressMessages(httptest::with_mock_api(code)), 12 | error = function(e) return(httptest::capture_requests(code)) 13 | )) 14 | }) 15 | } 16 | 17 | pkg_name <- function() tryCatch( 18 | pkgload::pkg_name(), 19 | error = function(e) return(getPackageName(search()[max(which(search() %in% c(".GlobalEnv", "devtools_shims")))+1])) 20 | ) 21 | -------------------------------------------------------------------------------- /inst/templates/tests/testthat/test-endpoint-plumber-{route_name}.R: -------------------------------------------------------------------------------- 1 | # Configuration ----------------------------------------------------------- 2 | config <- config::get(file = system.file("configurations", "plumber.yml", package = pkg_name(), mustWork = TRUE)) 3 | 4 | 5 | # Helpers ----------------------------------------------------------------- 6 | modify_url <- purrr::partial(httr::modify_url, url = "", scheme = config$scheme, hostname = config$host, port = config$port) 7 | expect_success_status <- function(response) expect_equal(httr::status_code(response), 200) 8 | expect_bad_request_status <- function(response) expect_equal(httr::status_code(response), 400) 9 | extract_content_text <- purrr::partial(httr::content, as = "text", encoding = "UTF-8") 10 | 11 | 12 | # list_tables ------------------------------------------------------------- 13 | test_http("list_tables returns a vector with table names", { 14 | url <- modify_url(path = c("route_name", "list_tables")) 15 | expect_success_status(response <- httr::GET(url)) 16 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 17 | expect_type(output, "character") 18 | expect_true("mtcars" %in% output) 19 | }) 20 | 21 | 22 | # read_table -------------------------------------------------------------- 23 | test_http("read_table returns a data.frame", { 24 | url <- modify_url(path = c("route_name", "read_table")) 25 | # Query an existing table 26 | expect_success_status(response <- httr::GET(url, query = list(name = "mtcars"))) 27 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 28 | expect_s3_class(output, "data.frame") 29 | 30 | }) 31 | 32 | test_http("read_table returns an informative error message", { 33 | url <- modify_url(path = c("route_name", "read_table")) 34 | # Query a non-existing table 35 | expect_bad_request_status(response <- httr::GET(url, query = list(name = "xxx"))) 36 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 37 | expect_match(output$error, "should be one of") 38 | }) 39 | 40 | 41 | # write_table ------------------------------------------------------------- 42 | test_http("write_table copies a data.frame to the route_name", { 43 | url <- modify_url(path = c("route_name", "write_table")) 44 | body <- list(name = "zzz", value = datasets::sleep) 45 | 46 | expect_success_status(response <- httr::POST(url, body = body, encode = "json")) 47 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 48 | expect_equivalent(output, list()) 49 | 50 | url <- modify_url(path = c("route_name", "list_tables")) 51 | expect_success_status(response <- httr::GET(url)) 52 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 53 | expect_true("zzz" %in% output) 54 | }) 55 | 56 | -------------------------------------------------------------------------------- /inst/templates/tests/testthat/test-plumber-microservice.R: -------------------------------------------------------------------------------- 1 | # Setup ------------------------------------------------------------------- 2 | expect_file_exists <- function(file) expect_true(file.exists(file), label = paste("does", basename(file), "exist?")) 3 | path <- tempfile() 4 | name <- "db" 5 | withr::defer(unlink(path)) 6 | 7 | 8 | # use_microservice -------------------------------------------------------- 9 | test_that("runs without errors",{ 10 | expect_silent(use_microservice(path = path)) 11 | }) 12 | 13 | test_that("copies all files",{ 14 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 15 | files <- file.path(path, config::get("use_microservice", file = file_fs)$files$add) 16 | for(file in files) expect_file_exists(glue::glue(file, route_name = "utility")) 17 | }) 18 | 19 | test_that("adds all package dependencies",{ 20 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 21 | packages <- config::get("use_microservice", file = file_fs)$dependencies 22 | desc <- desc::description$new(file.path(path, "DESCRIPTION")) 23 | 24 | expected_dependencies <- as.data.frame(config::get("use_microservice", file = file_fs)$dependencies) 25 | actual_dependencies <- desc$get_deps() 26 | 27 | expect_true( 28 | length(setdiff(expected_dependencies$package, actual_dependencies$package)) == 0, 29 | label = "Does DESCRIPTION include all the necessary package dependency?" 30 | ) 31 | }) 32 | 33 | 34 | # add_service ------------------------------------------------------------- 35 | test_that("fails if there is no prior service deployed",{ 36 | expect_error(add_service(path = tempfile(), name = name), "use_microservice") 37 | }) 38 | 39 | test_that("runs without errors",{ 40 | expect_null(add_service(path = path, name = name)) 41 | }) 42 | 43 | test_that("mounts new service",{ 44 | file <- file.path(path, "inst/entrypoints/plumber-foreground.R") 45 | content <- paste(readLines(file), collapse = "\n") 46 | expect_match(content, name) 47 | }) 48 | 49 | test_that("creates new endpoint unit test",{ 50 | file <- file.path(path, glue::glue("tests/testthat/test-endpoint-plumber-{route_name}.R", route_name = name)) 51 | expect_file_exists(file) 52 | }) 53 | 54 | test_that("creates new endpoint script",{ 55 | file <- file.path(path, glue::glue("inst/endpoints/plumber-{route_name}.R", route_name = name)) 56 | expect_file_exists(file) 57 | }) 58 | -------------------------------------------------------------------------------- /microservices.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Yes 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 4 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageBuildArgs: --no-manual 22 | PackageBuildBinaryArgs: --no-manual 23 | PackageCheckArgs: --no-manual 24 | PackageRoxygenize: rd,collate,namespace,vignette 25 | 26 | UseNativePipeOperator: Yes 27 | -------------------------------------------------------------------------------- /pkgdown/.gitignore: -------------------------------------------------------------------------------- 1 | !*.png 2 | !*.ico 3 | !*.yml 4 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://tidylab.github.io/microservices/ 2 | title: "Use Microservices in R" 3 | authors: 4 | Harel Lustiger: 5 | href: https://github.com/tidylab 6 | Tidylab: 7 | href: "https://github.com/tidylab" 8 | html: "" 9 | 10 | 11 | # Articles ---------------------------------------------------------------- 12 | articles: 13 | - title: "Background" 14 | contents: 15 | - articles/01-introduction 16 | - title: "Basic Useage" 17 | contents: 18 | - articles/02-plumber 19 | 20 | 21 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidylab/microservices/12441261b496213d2544e04d1bc7db86ba6c4d86/pkgdown/logo.png -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(microservices) 3 | test_check("microservices") 4 | -------------------------------------------------------------------------------- /tests/testthat/_api/127.0.0.1-8080/route_name/list_tables.json: -------------------------------------------------------------------------------- 1 | [ 2 | "ability.cov", 3 | "airmiles", 4 | "AirPassengers", 5 | "airquality", 6 | "anscombe", 7 | "attenu", 8 | "attitude", 9 | "austres", 10 | "BJsales", 11 | "BJsales.lead", 12 | "BOD", 13 | "cars", 14 | "ChickWeight", 15 | "chickwts", 16 | "co2", 17 | "CO2", 18 | "crimtab", 19 | "discoveries", 20 | "DNase", 21 | "esoph", 22 | "euro", 23 | "euro.cross", 24 | "eurodist", 25 | "EuStockMarkets", 26 | "faithful", 27 | "Formaldehyde", 28 | "freeny", 29 | "freeny.x", 30 | "freeny.y", 31 | "HairEyeColor", 32 | "Harman23.cor", 33 | "Harman74.cor", 34 | "Indometh", 35 | "infert", 36 | "InsectSprays", 37 | "iris", 38 | "iris3", 39 | "islands", 40 | "JohnsonJohnson", 41 | "LakeHuron", 42 | "lh", 43 | "LifeCycleSavings", 44 | "Loblolly", 45 | "longley", 46 | "lynx", 47 | "morley", 48 | "mtcars", 49 | "nhtemp", 50 | "Nile", 51 | "nottem", 52 | "npk", 53 | "occupationalStatus", 54 | "Orange", 55 | "OrchardSprays", 56 | "PlantGrowth", 57 | "precip", 58 | "presidents", 59 | "pressure", 60 | "Puromycin", 61 | "quakes", 62 | "randu", 63 | "rivers", 64 | "rock", 65 | "Seatbelts", 66 | "sleep", 67 | "stack.loss", 68 | "stack.x", 69 | "stackloss", 70 | "sunspot.month", 71 | "sunspot.year", 72 | "sunspots", 73 | "swiss", 74 | "Theoph", 75 | "Titanic", 76 | "ToothGrowth", 77 | "treering", 78 | "trees", 79 | "UCBAdmissions", 80 | "UKDriverDeaths", 81 | "UKgas", 82 | "USAccDeaths", 83 | "USArrests", 84 | "UScitiesD", 85 | "USJudgeRatings", 86 | "USPersonalExpenditure", 87 | "uspop", 88 | "VADeaths", 89 | "volcano", 90 | "warpbreaks", 91 | "women", 92 | "WorldPhones", 93 | "WWWusage", 94 | "zzz" 95 | ] 96 | -------------------------------------------------------------------------------- /tests/testthat/_api/127.0.0.1-8080/route_name/read_table-7376d4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mpg": 21, 4 | "cyl": 6, 5 | "disp": 160, 6 | "hp": 110, 7 | "drat": 3.9, 8 | "wt": 2.62, 9 | "qsec": 16.46, 10 | "vs": 0, 11 | "am": 1, 12 | "gear": 4, 13 | "carb": 4, 14 | "_row": "Mazda RX4" 15 | }, 16 | { 17 | "mpg": 21, 18 | "cyl": 6, 19 | "disp": 160, 20 | "hp": 110, 21 | "drat": 3.9, 22 | "wt": 2.875, 23 | "qsec": 17.02, 24 | "vs": 0, 25 | "am": 1, 26 | "gear": 4, 27 | "carb": 4, 28 | "_row": "Mazda RX4 Wag" 29 | }, 30 | { 31 | "mpg": 22.8, 32 | "cyl": 4, 33 | "disp": 108, 34 | "hp": 93, 35 | "drat": 3.85, 36 | "wt": 2.32, 37 | "qsec": 18.61, 38 | "vs": 1, 39 | "am": 1, 40 | "gear": 4, 41 | "carb": 1, 42 | "_row": "Datsun 710" 43 | }, 44 | { 45 | "mpg": 21.4, 46 | "cyl": 6, 47 | "disp": 258, 48 | "hp": 110, 49 | "drat": 3.08, 50 | "wt": 3.215, 51 | "qsec": 19.44, 52 | "vs": 1, 53 | "am": 0, 54 | "gear": 3, 55 | "carb": 1, 56 | "_row": "Hornet 4 Drive" 57 | }, 58 | { 59 | "mpg": 18.7, 60 | "cyl": 8, 61 | "disp": 360, 62 | "hp": 175, 63 | "drat": 3.15, 64 | "wt": 3.44, 65 | "qsec": 17.02, 66 | "vs": 0, 67 | "am": 0, 68 | "gear": 3, 69 | "carb": 2, 70 | "_row": "Hornet Sportabout" 71 | }, 72 | { 73 | "mpg": 18.1, 74 | "cyl": 6, 75 | "disp": 225, 76 | "hp": 105, 77 | "drat": 2.76, 78 | "wt": 3.46, 79 | "qsec": 20.22, 80 | "vs": 1, 81 | "am": 0, 82 | "gear": 3, 83 | "carb": 1, 84 | "_row": "Valiant" 85 | }, 86 | { 87 | "mpg": 14.3, 88 | "cyl": 8, 89 | "disp": 360, 90 | "hp": 245, 91 | "drat": 3.21, 92 | "wt": 3.57, 93 | "qsec": 15.84, 94 | "vs": 0, 95 | "am": 0, 96 | "gear": 3, 97 | "carb": 4, 98 | "_row": "Duster 360" 99 | }, 100 | { 101 | "mpg": 24.4, 102 | "cyl": 4, 103 | "disp": 146.7, 104 | "hp": 62, 105 | "drat": 3.69, 106 | "wt": 3.19, 107 | "qsec": 20, 108 | "vs": 1, 109 | "am": 0, 110 | "gear": 4, 111 | "carb": 2, 112 | "_row": "Merc 240D" 113 | }, 114 | { 115 | "mpg": 22.8, 116 | "cyl": 4, 117 | "disp": 140.8, 118 | "hp": 95, 119 | "drat": 3.92, 120 | "wt": 3.15, 121 | "qsec": 22.9, 122 | "vs": 1, 123 | "am": 0, 124 | "gear": 4, 125 | "carb": 2, 126 | "_row": "Merc 230" 127 | }, 128 | { 129 | "mpg": 19.2, 130 | "cyl": 6, 131 | "disp": 167.6, 132 | "hp": 123, 133 | "drat": 3.92, 134 | "wt": 3.44, 135 | "qsec": 18.3, 136 | "vs": 1, 137 | "am": 0, 138 | "gear": 4, 139 | "carb": 4, 140 | "_row": "Merc 280" 141 | }, 142 | { 143 | "mpg": 17.8, 144 | "cyl": 6, 145 | "disp": 167.6, 146 | "hp": 123, 147 | "drat": 3.92, 148 | "wt": 3.44, 149 | "qsec": 18.9, 150 | "vs": 1, 151 | "am": 0, 152 | "gear": 4, 153 | "carb": 4, 154 | "_row": "Merc 280C" 155 | }, 156 | { 157 | "mpg": 16.4, 158 | "cyl": 8, 159 | "disp": 275.8, 160 | "hp": 180, 161 | "drat": 3.07, 162 | "wt": 4.07, 163 | "qsec": 17.4, 164 | "vs": 0, 165 | "am": 0, 166 | "gear": 3, 167 | "carb": 3, 168 | "_row": "Merc 450SE" 169 | }, 170 | { 171 | "mpg": 17.3, 172 | "cyl": 8, 173 | "disp": 275.8, 174 | "hp": 180, 175 | "drat": 3.07, 176 | "wt": 3.73, 177 | "qsec": 17.6, 178 | "vs": 0, 179 | "am": 0, 180 | "gear": 3, 181 | "carb": 3, 182 | "_row": "Merc 450SL" 183 | }, 184 | { 185 | "mpg": 15.2, 186 | "cyl": 8, 187 | "disp": 275.8, 188 | "hp": 180, 189 | "drat": 3.07, 190 | "wt": 3.78, 191 | "qsec": 18, 192 | "vs": 0, 193 | "am": 0, 194 | "gear": 3, 195 | "carb": 3, 196 | "_row": "Merc 450SLC" 197 | }, 198 | { 199 | "mpg": 10.4, 200 | "cyl": 8, 201 | "disp": 472, 202 | "hp": 205, 203 | "drat": 2.93, 204 | "wt": 5.25, 205 | "qsec": 17.98, 206 | "vs": 0, 207 | "am": 0, 208 | "gear": 3, 209 | "carb": 4, 210 | "_row": "Cadillac Fleetwood" 211 | }, 212 | { 213 | "mpg": 10.4, 214 | "cyl": 8, 215 | "disp": 460, 216 | "hp": 215, 217 | "drat": 3, 218 | "wt": 5.424, 219 | "qsec": 17.82, 220 | "vs": 0, 221 | "am": 0, 222 | "gear": 3, 223 | "carb": 4, 224 | "_row": "Lincoln Continental" 225 | }, 226 | { 227 | "mpg": 14.7, 228 | "cyl": 8, 229 | "disp": 440, 230 | "hp": 230, 231 | "drat": 3.23, 232 | "wt": 5.345, 233 | "qsec": 17.42, 234 | "vs": 0, 235 | "am": 0, 236 | "gear": 3, 237 | "carb": 4, 238 | "_row": "Chrysler Imperial" 239 | }, 240 | { 241 | "mpg": 32.4, 242 | "cyl": 4, 243 | "disp": 78.7, 244 | "hp": 66, 245 | "drat": 4.08, 246 | "wt": 2.2, 247 | "qsec": 19.47, 248 | "vs": 1, 249 | "am": 1, 250 | "gear": 4, 251 | "carb": 1, 252 | "_row": "Fiat 128" 253 | }, 254 | { 255 | "mpg": 30.4, 256 | "cyl": 4, 257 | "disp": 75.7, 258 | "hp": 52, 259 | "drat": 4.93, 260 | "wt": 1.615, 261 | "qsec": 18.52, 262 | "vs": 1, 263 | "am": 1, 264 | "gear": 4, 265 | "carb": 2, 266 | "_row": "Honda Civic" 267 | }, 268 | { 269 | "mpg": 33.9, 270 | "cyl": 4, 271 | "disp": 71.1, 272 | "hp": 65, 273 | "drat": 4.22, 274 | "wt": 1.835, 275 | "qsec": 19.9, 276 | "vs": 1, 277 | "am": 1, 278 | "gear": 4, 279 | "carb": 1, 280 | "_row": "Toyota Corolla" 281 | }, 282 | { 283 | "mpg": 21.5, 284 | "cyl": 4, 285 | "disp": 120.1, 286 | "hp": 97, 287 | "drat": 3.7, 288 | "wt": 2.465, 289 | "qsec": 20.01, 290 | "vs": 1, 291 | "am": 0, 292 | "gear": 3, 293 | "carb": 1, 294 | "_row": "Toyota Corona" 295 | }, 296 | { 297 | "mpg": 15.5, 298 | "cyl": 8, 299 | "disp": 318, 300 | "hp": 150, 301 | "drat": 2.76, 302 | "wt": 3.52, 303 | "qsec": 16.87, 304 | "vs": 0, 305 | "am": 0, 306 | "gear": 3, 307 | "carb": 2, 308 | "_row": "Dodge Challenger" 309 | }, 310 | { 311 | "mpg": 15.2, 312 | "cyl": 8, 313 | "disp": 304, 314 | "hp": 150, 315 | "drat": 3.15, 316 | "wt": 3.435, 317 | "qsec": 17.3, 318 | "vs": 0, 319 | "am": 0, 320 | "gear": 3, 321 | "carb": 2, 322 | "_row": "AMC Javelin" 323 | }, 324 | { 325 | "mpg": 13.3, 326 | "cyl": 8, 327 | "disp": 350, 328 | "hp": 245, 329 | "drat": 3.73, 330 | "wt": 3.84, 331 | "qsec": 15.41, 332 | "vs": 0, 333 | "am": 0, 334 | "gear": 3, 335 | "carb": 4, 336 | "_row": "Camaro Z28" 337 | }, 338 | { 339 | "mpg": 19.2, 340 | "cyl": 8, 341 | "disp": 400, 342 | "hp": 175, 343 | "drat": 3.08, 344 | "wt": 3.845, 345 | "qsec": 17.05, 346 | "vs": 0, 347 | "am": 0, 348 | "gear": 3, 349 | "carb": 2, 350 | "_row": "Pontiac Firebird" 351 | }, 352 | { 353 | "mpg": 27.3, 354 | "cyl": 4, 355 | "disp": 79, 356 | "hp": 66, 357 | "drat": 4.08, 358 | "wt": 1.935, 359 | "qsec": 18.9, 360 | "vs": 1, 361 | "am": 1, 362 | "gear": 4, 363 | "carb": 1, 364 | "_row": "Fiat X1-9" 365 | }, 366 | { 367 | "mpg": 26, 368 | "cyl": 4, 369 | "disp": 120.3, 370 | "hp": 91, 371 | "drat": 4.43, 372 | "wt": 2.14, 373 | "qsec": 16.7, 374 | "vs": 0, 375 | "am": 1, 376 | "gear": 5, 377 | "carb": 2, 378 | "_row": "Porsche 914-2" 379 | }, 380 | { 381 | "mpg": 30.4, 382 | "cyl": 4, 383 | "disp": 95.1, 384 | "hp": 113, 385 | "drat": 3.77, 386 | "wt": 1.513, 387 | "qsec": 16.9, 388 | "vs": 1, 389 | "am": 1, 390 | "gear": 5, 391 | "carb": 2, 392 | "_row": "Lotus Europa" 393 | }, 394 | { 395 | "mpg": 15.8, 396 | "cyl": 8, 397 | "disp": 351, 398 | "hp": 264, 399 | "drat": 4.22, 400 | "wt": 3.17, 401 | "qsec": 14.5, 402 | "vs": 0, 403 | "am": 1, 404 | "gear": 5, 405 | "carb": 4, 406 | "_row": "Ford Pantera L" 407 | }, 408 | { 409 | "mpg": 19.7, 410 | "cyl": 6, 411 | "disp": 145, 412 | "hp": 175, 413 | "drat": 3.62, 414 | "wt": 2.77, 415 | "qsec": 15.5, 416 | "vs": 0, 417 | "am": 1, 418 | "gear": 5, 419 | "carb": 6, 420 | "_row": "Ferrari Dino" 421 | }, 422 | { 423 | "mpg": 15, 424 | "cyl": 8, 425 | "disp": 301, 426 | "hp": 335, 427 | "drat": 3.54, 428 | "wt": 3.57, 429 | "qsec": 14.6, 430 | "vs": 0, 431 | "am": 1, 432 | "gear": 5, 433 | "carb": 8, 434 | "_row": "Maserati Bora" 435 | }, 436 | { 437 | "mpg": 21.4, 438 | "cyl": 4, 439 | "disp": 121, 440 | "hp": 109, 441 | "drat": 4.11, 442 | "wt": 2.78, 443 | "qsec": 18.6, 444 | "vs": 1, 445 | "am": 1, 446 | "gear": 4, 447 | "carb": 2, 448 | "_row": "Volvo 142E" 449 | } 450 | ] 451 | -------------------------------------------------------------------------------- /tests/testthat/_api/127.0.0.1-8080/route_name/read_table-a26694.R: -------------------------------------------------------------------------------- 1 | structure(list(url = "http://127.0.0.1:8080/route_name/read_table?name=xxx", 2 | status_code = 400L, headers = structure(list(date = "Sat, 23 Oct 2021 00:54:58 GMT", 3 | `content-type` = "application/json", `content-length` = "1358"), class = c("insensitive", 4 | "list")), all_headers = list(list(status = 400L, version = "HTTP/1.1", 5 | headers = structure(list(date = "Sat, 23 Oct 2021 00:54:58 GMT", 6 | `content-type` = "application/json", `content-length` = "1358"), class = c("insensitive", 7 | "list")))), cookies = structure(list(domain = logical(0), 8 | flag = logical(0), path = logical(0), secure = logical(0), 9 | expiration = structure(numeric(0), class = c("POSIXct", 10 | "POSIXt")), name = logical(0), value = logical(0)), row.names = integer(0), class = "data.frame"), 11 | content = charToRaw("{\"error\":\"'arg' should be one of \\\"ability.cov\\\", \\\"airmiles\\\", \\\"AirPassengers\\\", \\\"airquality\\\", \\\"anscombe\\\", \\\"attenu\\\", \\\"attitude\\\", \\\"austres\\\", \\\"BJsales\\\", \\\"BJsales.lead\\\", \\\"BOD\\\", \\\"cars\\\", \\\"ChickWeight\\\", \\\"chickwts\\\", \\\"co2\\\", \\\"CO2\\\", \\\"crimtab\\\", \\\"discoveries\\\", \\\"DNase\\\", \\\"esoph\\\", \\\"euro\\\", \\\"euro.cross\\\", \\\"eurodist\\\", \\\"EuStockMarkets\\\", \\\"faithful\\\", \\\"Formaldehyde\\\", \\\"freeny\\\", \\\"freeny.x\\\", \\\"freeny.y\\\", \\\"HairEyeColor\\\", \\\"Harman23.cor\\\", \\\"Harman74.cor\\\", \\\"Indometh\\\", \\\"infert\\\", \\\"InsectSprays\\\", \\\"iris\\\", \\\"iris3\\\", \\\"islands\\\", \\\"JohnsonJohnson\\\", \\\"LakeHuron\\\", \\\"lh\\\", \\\"LifeCycleSavings\\\", \\\"Loblolly\\\", \\\"longley\\\", \\\"lynx\\\", \\\"morley\\\", \\\"mtcars\\\", \\\"nhtemp\\\", \\\"Nile\\\", \\\"nottem\\\", \\\"npk\\\", \\\"occupationalStatus\\\", \\\"Orange\\\", \\\"OrchardSprays\\\", \\\"PlantGrowth\\\", \\\"precip\\\", \\\"presidents\\\", \\\"pressure\\\", \\\"Puromycin\\\", \\\"quakes\\\", \\\"randu\\\", \\\"rivers\\\", \\\"rock\\\", \\\"Seatbelts\\\", \\\"sleep\\\", \\\"stack.loss\\\", \\\"stack.x\\\", \\\"stackloss\\\", \\\"sunspot.month\\\", \\\"sunspot.year\\\", \\\"sunspots\\\", \\\"swiss\\\", \\\"Theoph\\\", \\\"Titanic\\\", \\\"ToothGrowth\\\", \\\"treering\\\", \\\"trees\\\", \\\"UCBAdmissions\\\", \\\"UKDriverDeaths\\\", \\\"UKgas\\\", \\\"USAccDeaths\\\", \\\"USArrests\\\", \\\"UScitiesD\\\", \\\"USJudgeRatings\\\", \\\"USPersonalExpenditure\\\", \\\"uspop\\\", \\\"VADeaths\\\", \\\"volcano\\\", \\\"warpbreaks\\\", \\\"women\\\", \\\"WorldPhones\\\", \\\"WWWusage\\\", \\\"zzz\\\"\"}"), 12 | date = structure(1634950498, class = c("POSIXct", "POSIXt" 13 | ), tzone = "GMT"), times = c(redirect = 0, namelookup = 4.3e-05, 14 | connect = 4.4e-05, pretransfer = 0.00012, starttransfer = 0.00367, 15 | total = 0.003694)), class = "response") 16 | -------------------------------------------------------------------------------- /tests/testthat/_api/127.0.0.1-8080/route_name/write_table-10996a-POST.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /tests/testthat/helpers-xyz.R: -------------------------------------------------------------------------------- 1 | # httptest ---------------------------------------------------------------- 2 | options(httptest.debug = FALSE) 3 | 4 | test_http <- function(desc, code){ 5 | withr::local_package("httptest") 6 | old_dir <- httptest::.mockPaths()[1] 7 | on.exit(httptest::.mockPaths(old_dir)) 8 | httptest::.mockPaths("_api") 9 | testthat::test_that(desc, { 10 | suppressWarnings(tryCatch( 11 | suppressMessages(httptest::with_mock_api(code)), 12 | error = function(e) return(httptest::capture_requests(code)) 13 | )) 14 | }) 15 | } 16 | 17 | pkg_name <- function() tryCatch( 18 | pkgload::pkg_name(), 19 | error = function(e) return(getPackageName(search()[max(which(search() %in% c(".GlobalEnv", "devtools_shims")))+1])) 20 | ) 21 | -------------------------------------------------------------------------------- /tests/testthat/test-endpoint-plumber-{route_name}.R: -------------------------------------------------------------------------------- 1 | # Configuration ----------------------------------------------------------- 2 | config <- config::get(file = system.file("configurations", "plumber.yml", package = pkg_name(), mustWork = TRUE)) 3 | 4 | 5 | # Helpers ----------------------------------------------------------------- 6 | modify_url <- purrr::partial(httr::modify_url, url = "", scheme = config$scheme, hostname = config$host, port = config$port) 7 | expect_success_status <- function(response) expect_equal(httr::status_code(response), 200) 8 | expect_bad_request_status <- function(response) expect_equal(httr::status_code(response), 400) 9 | extract_content_text <- purrr::partial(httr::content, as = "text", encoding = "UTF-8") 10 | 11 | 12 | # list_tables ------------------------------------------------------------- 13 | test_http("list_tables returns a vector with table names", { 14 | url <- modify_url(path = c("route_name", "list_tables")) 15 | expect_success_status(response <- httr::GET(url)) 16 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 17 | expect_type(output, "character") 18 | expect_true("mtcars" %in% output) 19 | }) 20 | 21 | 22 | # read_table -------------------------------------------------------------- 23 | test_http("read_table returns a data.frame", { 24 | url <- modify_url(path = c("route_name", "read_table")) 25 | # Query an existing table 26 | expect_success_status(response <- httr::GET(url, query = list(name = "mtcars"))) 27 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 28 | expect_s3_class(output, "data.frame") 29 | 30 | }) 31 | 32 | test_http("read_table returns an informative error message", { 33 | url <- modify_url(path = c("route_name", "read_table")) 34 | # Query a non-existing table 35 | expect_bad_request_status(response <- httr::GET(url, query = list(name = "xxx"))) 36 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 37 | expect_match(output$error, "should be one of") 38 | }) 39 | 40 | 41 | # write_table ------------------------------------------------------------- 42 | test_http("write_table copies a data.frame to the route_name", { 43 | url <- modify_url(path = c("route_name", "write_table")) 44 | body <- list(name = "zzz", value = datasets::sleep) 45 | 46 | expect_success_status(response <- httr::POST(url, body = body, encode = "json")) 47 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 48 | expect_equivalent(output, list()) 49 | 50 | url <- modify_url(path = c("route_name", "list_tables")) 51 | expect_success_status(response <- httr::GET(url)) 52 | output <- extract_content_text(response) |> jsonlite::fromJSON(flatten = TRUE) 53 | expect_true("zzz" %in% output) 54 | }) 55 | 56 | -------------------------------------------------------------------------------- /tests/testthat/test-plumber-microservice.R: -------------------------------------------------------------------------------- 1 | # Setup ------------------------------------------------------------------- 2 | expect_file_exists <- function(file) expect_true(file.exists(file), label = paste("does", basename(file), "exist?")) 3 | path <- tempfile() 4 | name <- "db" 5 | withr::defer(unlink(path)) 6 | 7 | 8 | # use_microservice -------------------------------------------------------- 9 | test_that("runs without errors",{ 10 | expect_silent(use_microservice(path = path)) 11 | }) 12 | 13 | test_that("copies all files",{ 14 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 15 | files <- file.path(path, config::get("use_microservice", file = file_fs)$files$add) 16 | for(file in files) expect_file_exists(glue::glue(file, route_name = "utility")) 17 | }) 18 | 19 | test_that("adds all package dependencies",{ 20 | file_fs <- system.file("configurations", "fs.yml", package = "microservices", mustWork = TRUE) 21 | packages <- config::get("use_microservice", file = file_fs)$dependencies 22 | desc <- desc::description$new(file.path(path, "DESCRIPTION")) 23 | 24 | expected_dependencies <- as.data.frame(config::get("use_microservice", file = file_fs)$dependencies) 25 | actual_dependencies <- desc$get_deps() 26 | 27 | expect_true( 28 | length(setdiff(expected_dependencies$package, actual_dependencies$package)) == 0, 29 | label = "Does DESCRIPTION include all the necessary package dependency?" 30 | ) 31 | }) 32 | 33 | 34 | # add_service ------------------------------------------------------------- 35 | test_that("fails if there is no prior service deployed",{ 36 | expect_error(add_service(path = tempfile(), name = name), "use_microservice") 37 | }) 38 | 39 | test_that("runs without errors",{ 40 | expect_null(add_service(path = path, name = name)) 41 | }) 42 | 43 | test_that("mounts new service",{ 44 | file <- file.path(path, "inst/entrypoints/plumber-foreground.R") 45 | content <- paste(readLines(file), collapse = "\n") 46 | expect_match(content, name) 47 | }) 48 | 49 | test_that("creates new endpoint unit test",{ 50 | file <- file.path(path, glue::glue("tests/testthat/test-endpoint-plumber-{route_name}.R", route_name = name)) 51 | expect_file_exists(file) 52 | }) 53 | 54 | test_that("creates new endpoint script",{ 55 | file <- file.path(path, glue::glue("inst/endpoints/plumber-{route_name}.R", route_name = name)) 56 | expect_file_exists(file) 57 | }) 58 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | !_common.R 4 | -------------------------------------------------------------------------------- /vignettes/_common.R: -------------------------------------------------------------------------------- 1 | suppressPackageStartupMessages( 2 | purrr::quietly( 3 | withr::with_dir( 4 | usethis::proj_get(), 5 | pkgload::load_all(export_all = !FALSE, helpers = FALSE, quiet = TRUE, warn_conflicts = FALSE) 6 | ) 7 | ) 8 | ) 9 | 10 | 11 | # global options ---------------------------------------------------------- 12 | options( 13 | verbose = FALSE, 14 | tidyverse.quiet = TRUE, 15 | usethis.quiet = TRUE 16 | ) 17 | 18 | 19 | # knitr ------------------------------------------------------------------- 20 | knitr::opts_knit$set( 21 | root.dir = usethis::proj_get() 22 | ) 23 | 24 | knitr::opts_chunk$set( 25 | collapse = TRUE, 26 | out.width = '100%', 27 | echo = FALSE, 28 | results = "markup", 29 | message = FALSE, 30 | warning = FALSE, 31 | cache = !TRUE, 32 | comment = "#>", 33 | fig.retina = 0.8, # figures are either vectors or 300 dpi diagrams 34 | dpi = 300, 35 | out.width = "70%", 36 | fig.align = 'center', 37 | fig.width = 6, 38 | fig.asp = 0.618, # 1 / phi 39 | fig.show = "hold", 40 | eval.after = 'fig.cap' # so captions can use link to demos 41 | ) 42 | 43 | knitr::knit_hooks$set( 44 | error = function(x, options) { 45 | paste('\n\n
', ( 46 | x 47 | |> stringr::str_replace_all('^#>\ Error in eval\\(expr, envir, enclos\\):', '**Caution:**') 48 | |> stringr::str_replace_all('#> ', '\n') 49 | ), 50 | '
', sep = '\n') 51 | }, 52 | warning = function(x, options) { 53 | paste('\n\n
', ( 54 | x 55 | |> stringr::str_replace_all('##', '\n') 56 | |> stringr::str_replace_all('^#>\ Warning:', '**Note:**') 57 | |> stringr::str_remove_all("#>") 58 | ), 59 | '
', sep = '\n') 60 | }, 61 | message = function(x, options) { 62 | paste('\n\n
', 63 | gsub('##|#>', '\n', paste("**Tip:**", x)), 64 | '
', sep = '\n') 65 | } 66 | ) 67 | 68 | 69 | # rmarkdown --------------------------------------------------------------- 70 | kable <- knitr::kable 71 | 72 | 73 | # helpers ----------------------------------------------------------------- 74 | read_snippet <- function(name) readLines(system.file("inst", "snippets", paste0(name,".R"), package = devtools::loaded_packages()[1,1])) 75 | read_lines <- function(...) readLines(system.file(..., package = devtools::loaded_packages()[1,1])) 76 | 77 | 78 | # regex ------------------------------------------------------------------- 79 | discard_comments <- function(string) return(string[!stringr::str_detect(string, "^#")]) 80 | discard_null <- function(string) string[!stringr::str_detect(string, "^NULL")] 81 | discard_empty_lines <- function(string) string[nchar(string)>0] 82 | 83 | 84 | # events ------------------------------------------------------------------ 85 | events <- new.env() 86 | -------------------------------------------------------------------------------- /vignettes/articles/01-introduction.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Wrinting a Microservice in R" 3 | bibliography: [] 4 | biblio-style: apalike 5 | link-citations: yes 6 | editor_options: 7 | markdown: 8 | wrap: 80 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | source(file.path(usethis::proj_get(), "vignettes", "_common.R")) 13 | ``` 14 | 15 | ## Why should I use Microservices? 16 | 17 | ## What is the nature of R Applications? 18 | 19 | * Most applications are used by a small amount of users, if any 20 | * Most applications are seldom used concurrently 21 | 22 | ## Why should I use a Microservices with R applications? 23 | 24 | A microservice is a piece of business functionality with clear interfaces. 25 | Implementing an Microservice around an R application, allows its integration to 26 | platforms such as Android applications, Python applications, and commercial 27 | off-the-shelf products (COTS) products. This is because, a typical 28 | microservice is a process that communicate over a network throughout HTTP 29 | requests. The caller, whether it is a Website, Android application or even 30 | another R application, is neither aware nor care about the programming language 31 | that powers the service. 32 | 33 | ```{r error=TRUE} 34 | stop("Adding a Microservice increases complexity. You must have good reasons to do so.") 35 | ``` 36 | 37 | ```{r message=TRUE} 38 | message(" 39 | Good reasons to add a Microservice: 40 | 41 | 1. Scalability for non-uniform traffic 42 | 2. Error resilience 43 | 3. Separate deployments 44 | 4. Complete isolation 45 | 5. Different requirements 46 | ") 47 | ``` 48 | 49 | 50 | ## Microsrvice as an R package 51 | 52 | We can write a microservice as an [R 53 | extension](https://cran.r-project.org/doc/manuals/r-release/R-exts.html), 54 | specifically in a pseudo-package manner. In this mode, we take advantage of 55 | well-documented practice and mature toolkit designated to develop R packages. To 56 | be clear, our microservice is not an R package, rather it is built as an R 57 | package. Writing a microservice as an R package means: 58 | 59 | 1. We use the standard R package folder structure as a cookie cutter; 60 | 2. We develop (write, document and test) the microservice functionally as they 61 | were functions of an R package. 62 | 3. We use mature, i.e. existing and well-established, toolkit to facilitate the 63 | development. 64 | 65 | While development of microservices and R packages share folder structure and 66 | toolkit, they have a distinctly different purpose. In short, R packages "serve" 67 | microservices, but not the converse. Rather, microservices are stand-alone 68 | applications that *serve* a user or another system. Microservices are built 69 | primarily be practitioners. 70 | 71 | ## Layering the Microservice Functionallity 72 | 73 | ### Seperation of Concerns 74 | 75 | We can separate the concern of microservice functions into two: 76 | 77 | 1. Core-domain functions. These functions are not aware of the API. 78 | 2. API functions. These functions wrap core-domain functions. 79 | 80 | ### Where does each function belong? 81 | 82 | Core functions belong under the `R` folder. For example, the function 83 | `demo$class_input` (see excerpt) is part of `R/demo.R`. This function receives 84 | any object and returns its class. It can operate without an API, e.g. calling 85 | `demo$class_input(mtcars)` returns `data.frame`. 86 | 87 | ```{r, eval=FALSE, echo = TRUE} 88 | demo <- new.env() 89 | 90 | demo$class_input <- function(x = NULL){ 91 | return(class(x)) 92 | } 93 | ``` 94 | 95 | API function, including entrypoints and endpoints, are stored outside the `R` 96 | folder. In this demonstration they are stored under `inst/entrypoints` and 97 | `inst/endpoints`, respectively. We discuss in entrypoints and endpoints in 98 | length in subsequent documentations. For now, an example for an endpoint that 99 | wraps `demo$class_input` is a nameless function located under 100 | `inst/endpoints/plumber.R`: 101 | 102 | ```{r, eval=FALSE, echo = TRUE} 103 | function(x = NULL){ 104 | x <- x |> jsonlite::fromJSON(flatten = TRUE) 105 | demo$class_input(x) 106 | } 107 | ``` 108 | 109 | As you can see, the nameless function knows about the existence of 110 | `demo$class_input`, but not the converse. 111 | -------------------------------------------------------------------------------- /vignettes/articles/02-plumber.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Implementing a Microservice with Plumber" 3 | bibliography: [] 4 | biblio-style: apalike 5 | link-citations: yes 6 | editor_options: 7 | markdown: 8 | wrap: 80 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | source(usethis::proj_path("vignettes", "_common.R")) 13 | ``` 14 | 15 | ## Running Microservices with **plumber** API 16 | 17 | ### The Elements of **plumber** 18 | 19 | 1. [Foreground and Background Servers] 20 | 2. Endpoint 21 | 3. R Functions 22 | 23 | ### Foreground and Background Servers 24 | 25 | This package comes with working out-of-the-box **plumber** microservice. There 26 | are two options to spin-up the service: 27 | 28 | 1. `entrypoints/plumber-foreground.R` runs at the foreground; and 29 | 2. `entrypoints/plumber-background.R` runs at the background. 30 | 31 | Originally **plumber** runs in the foreground. That means the microservice locks 32 | down the session from running CLI commands or scripts. While this configuration 33 | is fine during deployment, on a dedicated server, it severely impedes 34 | development. We circumvent that hindrance by sourcing the microservice as a 35 | local job in RStudio. 36 | 37 | ### Dissecting the plumber Entrypoint 38 | 39 | The commands to run the demo microservice with a **plumber** API in the 40 | foreground are: 41 | 42 | ```{r code=read_lines("entrypoints","plumber-foreground.R"), eval=FALSE, echo=TRUE} 43 | ``` 44 | 45 | 1. `plumber::Plumber$new` creates a new Plumber router object. It requires a 46 | path to an endpoint file, which we discuss later in this document. 47 | 2. `plumber$setDocsCallback` by default plumber opens a browser with [OpenAPI 48 | Specification](https://swagger.io/specification/). OpenAPI tells the 49 | microservice users what URI exists, what are their parameters. In addition, 50 | OpenAPI allows users to fiddle around with API requests directly from 51 | through its GUI. While OpenAPI is highly useful, it fails when launching the 52 | microservice in the background. Instead, we nullify the callback function , 53 | and the user can manually browse the visual documentation at 54 | . 55 | 3. `plumber$run` spins up the microservice at the given `host` and `port`. 56 | 57 | As a design choice, we store the service configuration on a yaml file and load 58 | it to the global environment before calling `plumber$run`. The configuration 59 | file is located in `config/r-config.yml` and its details are: 60 | 61 | ```{r code=read_lines("inst","config","r-config.yml"), eval=FALSE, echo=TRUE} 62 | ``` 63 | 64 | ### Dissecting the plumber Endpoint 65 | 66 | In the context of **plumber**, an endpoint is an R script with one or more 67 | functions that respond to particular requests. An example for such function can 68 | be found in the demo's endpoint at `endpoints/plumber.R`: 69 | 70 | ```{e eval = FALSE, echo = TRUE} 71 | # Global code; gets executed at plumb() time. 72 | pkgload::load_all() 73 | 74 | #* Return input class 75 | #* Return the class of the input. 76 | #* @param x Any R data structure. 77 | #* @get utility/class 78 | function(x = NULL){ 79 | x <- x |> jsonlite::fromJSON(flatten = TRUE) 80 | demo$class_input(x) 81 | } 82 | ``` 83 | 84 | In this example, there is a nameless function that will respond to GET requests 85 | at the URN `/utility/class`. Say if the microservice URL is 86 | `http://127.0.0.1:8080`, then the complete function address (URI) is 87 | `http://127.0.0.1:8080/utility/class`. 88 | 89 | This endpoint excerpt emphasis two function are beyond the standard 90 | recommendations for **plumber**: 91 | 92 | 1. `pkgload::load_all()` makes the functions of the microservice (in the 93 | excerpt case its `demo$class_input`) available to the endpoint. Originally, 94 | `pkgload::load_all()` is used for package development in R. Calling the 95 | function simulates the package under development as it were installed and 96 | loaded in R. In the excerpt case, `demo$class_input` lives under the "R" 97 | folder, and is loaded\ 98 | 2. `jsonlite::fromJSON` parses the received input into a familiar R object, 99 | such as a data.frame, list or some atomic data structure. Without this 100 | explicit call, the endpoint might misinterpret its input argument, a JSON 101 | string, as a character scalar. 102 | 103 | You can find more information about **plumber** endpoints at the [package's 104 | website](https://www.rplumber.io/articles/routing-and-input.html). 105 | 106 | ## Communicating with Microservice 107 | 108 | ### Inspecting **plumber** Behaviour 109 | 110 | This demo include three utility URIs: 111 | 112 | 1. `127.0.0.1:8080/utility/healthcheck` returns status 200 if server is live 113 | and responding. 114 | 2. `127.0.0.1:8080/utility/class` returns the class of the object sent to the 115 | server. This is useful to validate a JSON string is parsed as expected on 116 | the microservice. 117 | 3. `127.0.0.1:8080/utility/mirror` returns the object that was sent to the 118 | server. This is useful to scrutinise the returning object from the 119 | microservice. 120 | 121 | ### Sending and Receiving JSON 122 | 123 | Send an object from R to the microservice with `jsonlite::toJSON`. For example, 124 | sending mtcars to `utility/mirror`: 125 | 126 | ```{r toJSON, eval = FALSE, echo=TRUE} 127 | input <- mtcars |> tibble::rownames_to_column() 128 | x <- jsonlite::toJSON(input, auto_unbox = TRUE) 129 | url <- URLencode(paste0("http://localhost:808/utility/mirror?x=", x)) 130 | response <- httr::GET(url) 131 | ``` 132 | 133 | Parse an object returning from the microservice with `jsonlite::fromJSON`, For 134 | example, parsing the mtcars returning from `utility/mirror`: 135 | 136 | ```{r fromJSON, eval = FALSE, echo=TRUE} 137 | output <- 138 | httr::content(response) |> 139 | jsonlite::fromJSON() 140 | ``` 141 | -------------------------------------------------------------------------------- /vignettes/articles/microservices.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Quickstart Guide" 3 | bibliography: [] 4 | biblio-style: apalike 5 | link-citations: yes 6 | editor_options: 7 | markdown: 8 | wrap: 80 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | source(usethis::proj_path("vignettes", "_common.R")) 13 | ``` 14 | 15 | ```{r internal-testing, eval=FALSE, include = FALSE} 16 | path <- tempfile("microservice") 17 | usethis::create_package(path) 18 | unlink(path, recursive = TRUE, force = TRUE) 19 | ``` 20 | 21 | In this quick start guide you'll learn how to get a rudimentary microservice 22 | working out of the box. 23 | 24 | ## Workflow 25 | 26 | ```{r child = "vignettes/excerpts/_setup-rudimentary-microservice.Rmd"} 27 | ``` 28 | 29 | ```{=html} 30 | 36 | ``` 37 | -------------------------------------------------------------------------------- /vignettes/details/add_service.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "add_service" 3 | editor_options: 4 | markdown: 5 | wrap: 80 6 | --- 7 | 8 | ```{r, include=FALSE, echo=FALSE, results='hide'} 9 | source(usethis::proj_path("vignettes", "_common.R")) 10 | 11 | path <<- "." 12 | name <<- "repository" 13 | 14 | file_fs <- system.file( 15 | "inst", "configurations", "fs.yml", 16 | package = "microservices", 17 | mustWork = TRUE 18 | ) 19 | 20 | info <- config::get("add_service", file = file_fs) 21 | 22 | microservice <- list( 23 | files = info$files, 24 | dependencies = as.data.frame(info$dependencies) 25 | ) 26 | 27 | microservice$files$add <- purrr::walk( 28 | microservice$files$add, 29 | glue::glue, 30 | route_name = name 31 | ) 32 | ``` 33 | 34 | Lay the infrastructure for an additional set of services. That includes adding a 35 | unit test, adding an endpoint, and extending the entrypointy. 36 | 37 | ```{r warning=TRUE} 38 | warning("`add_service` adds a service to pre-existing plumber microservice which you could deploy by calling `use_microservice`.") 39 | ``` 40 | 41 | ## How It Works 42 | 43 | Given a `path` to a folder and a service `name` 44 | 45 | When `add_service(path, name)` is called 46 | 47 | Then the function creates the following files: 48 | 49 | ```{r, code=microservice$files$add, eval=FALSE, echo=TRUE} 50 | ``` 51 | 52 | And updates the following files: 53 | 54 | ```{r, code=microservice$files$update, eval=FALSE, echo=TRUE} 55 | ``` 56 | 57 | ## When to Use 58 | 59 | In scenarios where services are thematically linked to each other. Examples for 60 | themes that should be mounted separately: 61 | 62 | - 'forecasting' and 'anomaly detection' 63 | - 'user' and 'business' 64 | -------------------------------------------------------------------------------- /vignettes/details/use_microservice.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "use_microservice" 3 | editor_options: 4 | markdown: 5 | wrap: 80 6 | --- 7 | 8 | ```{r, include = FALSE} 9 | source(usethis::proj_path("vignettes", "_common.R")) 10 | file_fs <- system.file("inst", "configurations", "fs.yml", package = "microservices", mustWork = TRUE) 11 | info <- config::get("use_microservice", file=file_fs) 12 | microservice <- list(files = info$files, dependencies = as.data.frame(info$dependencies)) 13 | ``` 14 | 15 | ## How It Works 16 | 17 | Given a `path` to a folder 18 | 19 | When `use_microservice(path = ".")` is called 20 | 21 | Then the function creates the following files: 22 | 23 | ```{r, code = microservice$files$add, eval = FALSE, echo=TRUE} 24 | ``` 25 | 26 | And updates the following files: 27 | 28 | ```{r, code = microservice$files$update, eval = FALSE, echo=TRUE} 29 | ``` 30 | 31 | And adds the following packages to the DESCRIPTION file: 32 | 33 | ```{r} 34 | kable(microservice$dependencies) 35 | ``` 36 | 37 | ## When to Use `plumber` 38 | 39 | - A Single user/machine applications. 40 | - Scheduled tasks. For example, you could use [AirFlow with HTTP 41 | Operators](https://airflow.apache.org/docs/apache-airflow-providers-http/stable/operators.html) 42 | to automate processes. 43 | 44 | ### `plumber` Advantages 45 | 46 | - Comes with familiar way to document the microservice endpoint. 47 | - Maturing package that comes with documentation, examples and support. 48 | 49 | ### `plumber` Disadvantages 50 | 51 | - Runs on a single thread. That means that parallel algorithms such as random 52 | forest, can only be run on one core. 53 | - Serves only one caller at a time. 54 | - Can't make inward calls for other services, That means plumber can't be 55 | [re-entrant](https://en.wikipedia.org/wiki/Reentrancy_(computing)). For 56 | example, if a microservice has three endpoints,`read_table`, `write_table`, 57 | and `orchestrator`, where the `orchestrator` reads a data table, transforms 58 | it, and writes it back, then the `orchestrator` can't make inwards calls via 59 | HTTP to `read_table` and `write_table`. 60 | 61 | ```{r, warning=TRUE} 62 | warning("While `plumber` is single-threaded by nature, it is possible to perform parallel execution using the `promises` package. See links under References.") 63 | ``` 64 | 65 | ## Workflow 66 | 67 | ```{r child = "vignettes/excerpts/_setup-rudimentary-microservice.Rmd"} 68 | ``` 69 | 70 | ## References 71 | 72 | - [Parallel execution in 73 | plumber](https://www.rstudio.com/blog/plumber-v1-1-0/#parallel-exec) 74 | - [`promises` 75 | package](https://rstudio.github.io/promises/articles/overview.html) 76 | -------------------------------------------------------------------------------- /vignettes/excerpts/_setup-rudimentary-microservice.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setup Rudimentary Microservice" 3 | bibliography: [] 4 | biblio-style: apalike 5 | link-citations: yes 6 | editor_options: 7 | markdown: 8 | wrap: 80 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | source(usethis::proj_path("vignettes", "_common.R")) 13 | ``` 14 | 15 | 1. Deploy the Microservice infrastructure 16 | 17 | ```{r, eval=FALSE, echo=TRUE} 18 | microservices::use_microservice(path = ".") 19 | remotes::install_deps(dependencies = TRUE) 20 | devtools::document() 21 | ``` 22 | 23 | 2. Spin-up the microservice by running 24 | `source("./inst/entrypoints/plumber-background.R")` 25 | 26 | 3. Run the microservice unit-test by pressing Ctrl+Shift+T on Windows 27 | 28 | Congratulations! You have added a microservice to your application and tested 29 | that it works. 30 | --------------------------------------------------------------------------------