Skip to content

Commit

Permalink
Add odata_entitylist_metadata_get
Browse files Browse the repository at this point in the history
  • Loading branch information
florianm committed Nov 5, 2024
1 parent 2262e4c commit 21a1c0a
Show file tree
Hide file tree
Showing 19 changed files with 468 additions and 3 deletions.
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand

S3method(print,odata_entitylist_metadata_get)
S3method(print,odata_entitylist_service_get)
S3method(print,ru_settings)
export("%>%")
Expand Down Expand Up @@ -63,6 +64,7 @@ export(handle_ru_datetimes)
export(handle_ru_geopoints)
export(handle_ru_geoshapes)
export(handle_ru_geotraces)
export(odata_entitylist_metadata_get)
export(odata_entitylist_service_get)
export(odata_metadata_get)
export(odata_service_get)
Expand Down
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* `entity_changes` now returns a tibble instead of nested list.
* `submission_export` gains a new parameter `deleted_fields` to export all
known fields of a form, including fields that were deleted in the latest form
version. (#129)
version. (#129, #161)


# ruODK 1.5.0
Expand Down
212 changes: 212 additions & 0 deletions R/odata_entitylist_metadata_get.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#' Get the Metadata Document from the OData Dataset Service.
#'
#' `r lifecycle::badge("experimental")`
#'
#' The Metadata Document describes, in
#' [EDMX CSDL](https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html),
#' the schema of all the data you can retrieve from the OData Dataset Service
#' in question. Essentially, these are the Dataset properties, or the schema of
#' each Entity, translated into the OData format.
#'
#' @template tpl-structure-nested
#' @template tpl-def-entitylist
#' @template tpl-entitylist-dataset
#' @template tpl-auth-missing
#' @template tpl-compat-2022-3
#' @template param-pid
#' @template param-did
#' @template param-url
#' @template param-auth
#' @template param-retries
#' @template param-odkcv
#' @template param-orders
#' @template param-tz
#' @return An S3 class `odata_entitylist_metadata_get` and `list` containing
#' the Metadata document following the DDMX CSDL standard
#'
#' * `version` The EDMX version, e.g. "4.0"
#' * `complex_types`
#' * `entity_types`
#' * `containers`
# nolint start
#' @seealso \url{https://docs.getodk.org/central-api-odata-endpoints/#id2}
#' @seealso \url{https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html}
# nolint end
#' @family entity-management
#' @export
#' @examples
#' \dontrun{
#' # See vignette("setup") for setup and authentication options
#' # ruODK::ru_setup(svc = "....svc", un = "[email protected]", pw = "...")
#'
#' ds <- entitylist_list(pid = get_default_pid())
#'
#' dm1 <- odata_entitylist_metadata_get(pid = get_default_pid(), did = ds$name[1])
#'
#' # Overview
#' print(dm1)
#'
#' # Get all property names for an entity type
#' names(dm1$entity_types$Entities$properties)
#'
#' # Check what properties are non-filterable
#' dm1$containers$trees$entity_sets$Entities$capabilities
#'
#' # Get complex type definitions
#' dm1$complex_types$metadata$properties
#' }
odata_entitylist_metadata_get <- function(pid = get_default_pid(),
did = "",
url = get_default_url(),
un = get_default_un(),
pw = get_default_pw(),
retries = get_retries(),
odkc_version = get_default_odkc_version(),
orders = get_default_orders(),
tz = get_default_tz()) {
yell_if_missing(url, un, pw, pid = pid, did = did)

if (odkc_version |> semver_lt("2022.3")) {
ru_msg_warn("odata_entitylist_service_get is supported from v2022.3")
}

doc <- httr::RETRY(
"GET",
httr::modify_url(url,
path = glue::glue(
"v1/projects/{pid}/datasets/",
"{URLencode(did, reserved = TRUE)}.svc/$metadata"
)
),
httr::add_headers(
"Accept" = "application/xml"
),
httr::authenticate(un, pw),
times = retries
) |>
yell_if_error(url, un, pw) |>
httr::content(encoding = "utf-8")

# Convert EDMX XML Document to structured R object

# Define the namespaces explicitly
ns <- c(
edmx = "http://docs.oasis-open.org/odata/ns/edmx",
edm = "http://docs.oasis-open.org/odata/ns/edm"
)

# Helper function to extract property information
extract_properties <- function(type_node) {
props <- xml2::xml_find_all(type_node, "./edm:Property", ns)
property_list <- lapply(props, function(prop) {
list(
name = xml2::xml_attr(prop, "Name"),
type = xml2::xml_attr(prop, "Type")
)
})
names(property_list) <- sapply(property_list, `[[`, "name")
property_list
}

# Extract complex types
complex_types <- xml2::xml_find_all(doc, "//edm:ComplexType", ns)
complex_types_list <- lapply(complex_types, function(type) {
list(
name = xml2::xml_attr(type, "Name"),
properties = extract_properties(type)
)
})
names(complex_types_list) <- sapply(complex_types_list, `[[`, "name")

# Extract entity types
entity_types <- xml2::xml_find_all(doc, "//edm:EntityType", ns)
entity_types_list <- lapply(entity_types, function(type) {
# Extract key properties
keys <- xml2::xml_find_all(type, ".//edm:PropertyRef", ns)
key_names <- sapply(keys, xml2::xml_attr, "Name")

list(
name = xml2::xml_attr(type, "Name"),
keys = key_names,
properties = extract_properties(type)
)
})
names(entity_types_list) <- sapply(entity_types_list, `[[`, "name")

# Extract entity container information
containers <- xml2::xml_find_all(doc, "//edm:EntityContainer", ns)
container_list <- lapply(containers, function(container) {
entity_sets <- xml2::xml_find_all(container, "./edm:EntitySet", ns)
sets_list <- lapply(entity_sets, function(set) {
# Extract capabilities from annotations
annotations <- xml2::xml_find_all(set, "./edm:Annotation", ns)
capabilities <- lapply(annotations, function(anno) {
term <- xml2::xml_attr(anno, "Term")
if (grepl("ConformanceLevel$", term)) {
list(
capability = "conformance_level",
value = xml2::xml_attr(anno, "EnumMember")
)
} else if (grepl("BatchSupported$", term)) {
list(
capability = "batch_supported",
value = as.logical(xml2::xml_attr(anno, "Bool"))
)
} else if (grepl("FilterRestrictions$", term)) {
# Extract non-filterable properties
non_filterable <- xml2::xml_find_all(anno, ".//edm:PropertyPath", ns)
if (length(non_filterable) > 0) {
list(
capability = "filter_restrictions",
non_filterable_properties = xml2::xml_text(non_filterable)
)
} else {
NULL
}
} else {
NULL
}
})
capabilities <- Filter(Negate(is.null), capabilities)

list(
name = xml2::xml_attr(set, "Name"),
type = xml2::xml_attr(set, "EntityType"),
capabilities = capabilities
)
})
names(sets_list) <- sapply(sets_list, `[[`, "name")

list(
name = xml2::xml_attr(container, "Name"),
entity_sets = sets_list
)
})
names(container_list) <- sapply(container_list, `[[`, "name")

# Get the root Edmx node to extract version
edmx_root <- xml2::xml_find_first(doc, "//edmx:Edmx", ns)
version <- xml2::xml_attr(edmx_root, "Version")

# Create the final structure
structure(list(
version = version,
complex_types = complex_types_list,
entity_types = entity_types_list,
containers = container_list
), class = c("odata_entitylist_metadata_get", "list"))
}

#' @export
print.odata_entitylist_metadata_get <- function(x, ...) {
cat("<ruODK OData EntityList Metadata>", sep = "\n")
cat(" Complex Types: ")
print(names(x$complex_types))
cat(" Entity Types: ")
print(names(x$entity_types))
cat(" Containers: ")
print(names(x$containers))
}


# usethis::use_test("odata_entitylist_metadata_get") # nolint
2 changes: 1 addition & 1 deletion R/odata_entitylist_service_get.R
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ odata_entitylist_service_get <- function(pid = get_default_pid(),
structure(list(
context = ds$odata_context,
value = purrr::map_df(ds$value, ~ tibble::as_tibble(.x))
), class = "odata_entitylist_service_get")
), class = c("odata_entitylist_service_get", "list"))
}

#' @export
Expand Down
1 change: 1 addition & 0 deletions man/entity_audits.Rd

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

1 change: 1 addition & 0 deletions man/entity_changes.Rd

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

1 change: 1 addition & 0 deletions man/entity_create.Rd

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

1 change: 1 addition & 0 deletions man/entity_delete.Rd

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

1 change: 1 addition & 0 deletions man/entity_detail.Rd

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

1 change: 1 addition & 0 deletions man/entity_list.Rd

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

1 change: 1 addition & 0 deletions man/entity_update.Rd

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

1 change: 1 addition & 0 deletions man/entity_versions.Rd

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

1 change: 1 addition & 0 deletions man/entitylist_detail.Rd

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

1 change: 1 addition & 0 deletions man/entitylist_download.Rd

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

1 change: 1 addition & 0 deletions man/entitylist_list.Rd

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

1 change: 1 addition & 0 deletions man/entitylist_update.Rd

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

Loading

0 comments on commit 21a1c0a

Please sign in to comment.