Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

jidea-56 Metadata endpoint #5

Merged
merged 13 commits into from
Aug 9, 2024
41 changes: 41 additions & 0 deletions R/api.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,44 @@
)
lapply(versions, function(v) scalar(as.character(v)))
}

# TODO: specify schema and rerun roxygen2::roxygenize()!!

Check warning on line 34 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=34,col=1,[todo_comment_linter] TODO comments should be removed.
##' @porcelain GET /metadata => json(metadata)
metadata <- function() {
# TODO: Use relevant model version - from qs if specified, else from latest available metadata file (jidea-62)

Check warning on line 37 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=37,col=3,[todo_comment_linter] TODO comments should be removed.

Check warning on line 37 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=37,col=81,[line_length_linter] Lines should not be more than 80 characters. This line is 112 characters.
model_version <- scalar("0.1.0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this pull from the package version for now? The model structure and data versions are all bundled together in the package version at the moment anyway.

# TODO: read in correct metadata version according to model_version (jidea-62)

Check warning on line 39 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=39,col=3,[todo_comment_linter] TODO comments should be removed.
metadata_file <- sprintf("metadata_%s.json", model_version)
response <- read_json(metadata_file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially confused as to where this function was coming from, but now I see it's redefined in this package; I've added a suggestion to switch to jsonlite::fromJSON() instead

response$modelVersion <- model_version

Check warning on line 43 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=43,col=1,[trailing_whitespace_linter] Trailing whitespace is superfluous.
# Helper for the options which don't come from the json
get_option <- function(id, label) {
list(id = scalar(id), label = scalar(label))
}

Check warning on line 48 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=48,col=1,[trailing_whitespace_linter] Trailing whitespace is superfluous.
# Set available countries from daedalus package
# TODO: use the right version of daedalus/model

Check warning on line 50 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=50,col=3,[todo_comment_linter] TODO comments should be removed.
# TODO: get ISO ids from daedalus when available

Check warning on line 51 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=51,col=3,[todo_comment_linter] TODO comments should be removed.
country_options <- lapply(daedalus::country_names, function(country) {
country_string <- as.character(country)
get_option(country_string, country_string)
})
country_idx <- match("country", response$parameters$id)
response$parameters$options[[country_idx]] <- country_options

Check warning on line 58 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=58,col=1,[trailing_whitespace_linter] Trailing whitespace is superfluous.
# TODO: get pathogen information from daedalus, when available (JIDEA-61)

Check warning on line 59 in R/api.R

View workflow job for this annotation

GitHub Actions / lint-changed-files

file=R/api.R,line=59,col=3,[todo_comment_linter] TODO comments should be removed.
pathogen_options <- list(
get_option("sars-cov-1", "SARS-CoV-1"),
get_option("sars-cov-2-pre-alpha", "SARS-CoV-2 pre-alpha (wildtype)"),
get_option("sars-cov-2-omicron", "SARS-CoV-2 omicron"),
get_option("sars-cov-2-delta", "SARS-CoV-2 delta"),
get_option("influenza-2009", "Influenza 2009 (Swine flu)"),
get_option("influenza-1957", "Influenza 1957"),
get_option("influenza-1918", "Influenza 1918 (Spanish flu)")
)
pathogen_idx <- match("pathogen", response$parameters$id)
response$parameters$options[[pathogen_idx]] <- pathogen_options

response
}
7 changes: 7 additions & 0 deletions R/parameters.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Allowed parameter ids which we can pass to the model
expected_parameters = c(
"country",
"pathogen",
"response",
"vaccine"
)
8 changes: 8 additions & 0 deletions R/porcelain.R

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions R/util.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,17 @@ scalar <- function(x) {
package_version_string <- function(name) {
as.character(utils::packageVersion(name))
}

system_file <- function(...) {
tryCatch({
system.file(..., mustWork = TRUE, package = "daedalus.api")
}, error = function(e) {
stop(sprintf("Failed to locate file from args\n%s",
paste(list(...), collapse = " ")))
})
}
Comment on lines +10 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handy method stolen from hintr!

Comment on lines +10 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why this function has been included, although it's over-writing a well known base function; the informative error is useful. Could we get a small comment explaining that?


read_json <- function(filename) {
json <- jsonlite::fromJSON(system_file("json", filename))
json
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be replaced by the {jsonlite} function of the same name, with simplifyVector = TRUE? Or alternatively, using jsonlite::fromJSON() directly? The wrapper being the same name as the {jsonlite} had me confused what it was doing (especially as the behaviour, e.g. simplifyVector is the opposite of what the {jsonlite} version does).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. How about if I change the name to something not shared with jsonlite to make it clear this is a special for the API? read_local_json_file? I'm just thinking that neither fromJSON nor read_json are going to do exactly the same as their jsonlite equivalent since it's selecting the folder to read from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_local_json() sounds good!

25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@

# daedalus.api

<!-- badges: start -->
[![Project Status: Concept – Minimal or no implementation has been done yet, or the repository is only intended to be a limited example, demo, or proof-of-concept.](https://www.repostatus.org/badges/latest/concept.svg)](https://www.repostatus.org/#concept)
[![CRAN status](https://www.r-pkg.org/badges/version/daedalus.api)](https://CRAN.R-project.org/package=daedalus.api)
[![Codecov test coverage](https://codecov.io/gh/jameel-institute/daedalus.api/branch/main/graph/badge.svg)](https://app.codecov.io/gh/jameel-institute/daedalus.api?branch=main)
[![R-CMD-check](https://github.com/jameel-institute/daedalus.api/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/jameel-institute/daedalus.api/actions/workflows/R-CMD-check.yaml)
[![Build status](https://badge.buildkite.com/2fe5d34f1b4c4681b4e0e8d464f4fdaf44358fc48325b92580.svg)](https://buildkite.com/mrc-ide/daedalus-dot-api)

[![Project Status: Concept -- Minimal or no implementation has been done yet, or the repository is only intended to be a limited example, demo, or proof-of-concept.](https://www.repostatus.org/badges/latest/concept.svg)](https://www.repostatus.org/#concept) [![CRAN status](https://www.r-pkg.org/badges/version/daedalus.api)](https://CRAN.R-project.org/package=daedalus.api) [![Codecov test coverage](https://codecov.io/gh/jameel-institute/daedalus.api/branch/main/graph/badge.svg)](https://app.codecov.io/gh/jameel-institute/daedalus.api?branch=main) [![R-CMD-check](https://github.com/jameel-institute/daedalus.api/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/jameel-institute/daedalus.api/actions/workflows/R-CMD-check.yaml) [![Build status](https://badge.buildkite.com/2fe5d34f1b4c4681b4e0e8d464f4fdaf44358fc48325b92580.svg)](https://buildkite.com/mrc-ide/daedalus-dot-api)

<!-- badges: end -->

_daedalus.api_ is an API package for the [_daedalus_ package](https://github.com/jameel-institute/daedalus) and is primarily intended for internal use.
Expand Down Expand Up @@ -37,6 +34,22 @@ curl -s http://localhost:8001 | jq
docker stop daedalus-api
```

## Development

To add an endpoint, implement a method in `api.R` with `@porcelain` comment, then run `roxygen2::roxygenize()` to generate the porcelain code
in `porcelain.R`. See the [porcelain docs](https://reside-ic.github.io/porcelain/articles/roxygen.html) for more details.

## Model versions

The API should be backwards compatible and support running older versions of the model.
Some endpoints support providing `modelVersion` as a query string parameter, e.g. to run or get metadata for a particular version of the model.

Metadata is stored in the `inst/json` folder, in files named `metadata_[VERSION].json` where `[VERSION]` is the first model version where
that metadata applied. Requesting metadata for a model version will return the metadata which applies to that version, (which may have been
first introduced in an earlier version). The metadata response includes a `modelVersion` property - this value will be the `modelVersion`
requested in the query string, if provided. If `modelVersion` was not provided in the query string, the returned
model version will be the most recent metadata's `[VERSION]`.

## Related projects

See the [_daedalus_ package](https://github.com/jameel-institute/daedalus) which implements the DAEDALUS integrated model of economic, social, and health costs of a pandemic.
47 changes: 47 additions & 0 deletions inst/json/metadata_0.1.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"modelVersion": null,
"parameters": [
{
"id": "country",
"label": "Country",
"parameterType": "globeSelect",
"defaultOption": null,
"ordered": false,
"options": null
},
{
"id": "pathogen",
"label": "Disease",
"parameterType": "select",
"defaultOption": null,
"ordered": false,
"options": null
},
{
"id": "response",
"label": "Response",
"parameterType": "select",
"defaultOption": "no_closure",
"ordered": true,
"options": [
{ "id": "no_closure", "label": "No closures" },
{ "id": "school_closure", "label": "School closures" },
{ "id": "business_closure", "label": "Business closures" },
{ "id": "elimination", "label": "Elimination" }
]
},
{
"id": "vaccine",
"label": "Advance vaccine investment",
"parameterType": "select",
"defaultOption": "none",
"ordered": true,
"options": [
{ "id": "none", "label": "None" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a question than a comment, but it's possible in the future we'll need to annotate each such option with a description to explain what precisely is meant by e.g. 'Medium'. I presume it's OK to cross that bridge when we get to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can add a "description" field if required.

{ "id": "low", "label": "Low" },
{ "id": "medium", "label": "Medium" },
{ "id": "high", "label": "High" }
]
}
]
}
44 changes: 44 additions & 0 deletions inst/schema/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"modelVersion": {
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)*$"
},
"parameters":{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"label": { "type": "string" },
"parameterType": {
"type": "string",
"enum": ["select", "globeSelect"]
},
"defaultOption": {
"type": ["string", "null"]
},
"ordered": { "type": "boolean" },
"options": {
"type": "array",
"items": {
"type": "object",
"properties":{
"id": { "type": "string" },
"label": { "type": "string" }
},
"additionalProperties": false,
"required": ["id", "label"]
}
}
},
"additionalProperties": false,
"required": ["id", "label", "parameterType", "defaultOption", "ordered", "options"]
}
}
},
"additionalProperties": false,
"required": ["modelVersion", "parameters"]
}
20 changes: 20 additions & 0 deletions tests/testthat/test-endpoints.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,23 @@ test_that("Can construct the api", {
expect_length(logs, 2L)
expect_identical(logs[[1L]]$logger, "daedalus.api")
})

test_that("Can get metadata", {
endpoint <- daedalus_api_endpoint("GET", "/metadata")
res <- endpoint$run()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could I suggest a no conditions expectation here:

Suggested change
res <- endpoint$run()
expect_no_condition(
res <- endpoint$run()
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, and I've put it on the root call too..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, that upsets the linter though: Avoid implicit assignments in function calls.
So have excluded those lines from linting!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, a classic. The other linter complaints can usually be fixed with a styler::style_pkg() call

expect_true(res$validated)

lapply(res$data$parameters$id, function(id){
expect_true(id %in% expected_parameters)
})

# expect country ids to match those from daedalus
country_idx <- match("country", res$data$parameters$id)
country_options <- res$data$parameters$options[[country_idx]]
daedalus_countries = daedalus::country_names
expect_equal(length(country_options), length(daedalus_countries))
lapply(seq_along(country_options), function(idx) {
expect_equal(country_options[[idx]]$id, scalar(daedalus_countries[[idx]]))
expect_equal(country_options[[idx]]$label, scalar(daedalus_countries[[idx]]))
})
})
Loading