From a8e9dd03ffda2161d45f1b693b4854c2cfd000ba Mon Sep 17 00:00:00 2001 From: Vincent Emonet Date: Tue, 30 Apr 2024 12:55:49 +0200 Subject: [PATCH] docs: use codeblocks tabs for the different languages --- lib/docs/docs/contributing.md | 14 +- lib/docs/docs/devtools.md | 267 ++++++ lib/docs/docs/getting-started.md | 893 ++++++++++++++++++ lib/docs/docs/index.md | 2 +- lib/docs/docs/javascript-example-framework.md | 2 +- lib/docs/docs/javascript-example-html.md | 2 +- lib/docs/docs/python-devtools.md | 140 --- lib/docs/docs/rust.md | 24 +- lib/docs/includes/abbreviations.md | 2 + lib/docs/mkdocs.yml | 40 +- lib/src/lib.rs | 5 + 11 files changed, 1221 insertions(+), 170 deletions(-) create mode 100644 lib/docs/docs/devtools.md create mode 100644 lib/docs/docs/getting-started.md delete mode 100644 lib/docs/docs/python-devtools.md diff --git a/lib/docs/docs/contributing.md b/lib/docs/docs/contributing.md index 5d943ff..96a7bba 100644 --- a/lib/docs/docs/contributing.md +++ b/lib/docs/docs/contributing.md @@ -162,14 +162,16 @@ cargo outdated ## ๐Ÿท๏ธ Publish a new release -Building and publishing artifacts will be done by the [`build.yml`](https://github.com/biopragmatics/curies.rs/actions/workflows/build.yml) GitHub actions workflow, make sure you have set the following tokens as secrets on GitHub for this repository: `PYPI_TOKEN`, `NPM_TOKEN`, `CRATES_IO_TOKEN`, `CODECOV_TOKEN` +!!! success "Automated release" + + Building and publishing artifacts (binaries, pip wheels, npm package) will be done automatically by the [`.github/workflows/build.yml`](https://github.com/biopragmatics/curies.rs/actions/workflows/build.yml) GitHub action when you push a new tag. + +!!! warning "Set secrets for the GitHub repository" + + Make sure you have set the following tokens as secrets on GitHub for this repository: `PYPI_TOKEN`, `NPM_TOKEN`, `CRATES_IO_TOKEN`, `CODECOV_TOKEN` -To release a new version, run the release script providing the new version following [semantic versioning](https://semver.org), it will bump the version in the `Cargo.toml` files, generate the changelog from commit messages, create a new tag, and push to GitHub: +To release a new version, run the release script providing the new version following [semantic versioning](https://semver.org), it will bump the version in the `Cargo.toml` files, generate the changelog from commit messages, create a new tag, and push to GitHub; the workflow will do the rest: ```bash ./scripts/release.sh 0.1.2 ``` - -!!! success "Automated release" - - The `build.yml` workflow will automatically build artifacts (binaries, pip wheels, npm package), create a new release on GitHub, and add the generated artifacts to the new release. diff --git a/lib/docs/docs/devtools.md b/lib/docs/docs/devtools.md new file mode 100644 index 0000000..5d2664c --- /dev/null +++ b/lib/docs/docs/devtools.md @@ -0,0 +1,267 @@ +# ๐Ÿงฐ Tools for Developers and Semantic Engineers + +## ๐Ÿช„ Working with strings that might be a URI or a CURIE + +Sometimes, itโ€™s not clear if a string is a CURIE or a URI. While the [SafeCURIE syntax](https://www.w3.org/TR/2010/NOTE-curie-20101216/#P_safe_curie) is intended to address this, itโ€™s often overlooked. + +### โ˜‘๏ธ CURIE and URI Checks + +The first way to handle this ambiguity is to be able to check if the string is a CURIE or a URI. Therefore, each `Converter` comes with functions for checking if a string is a CURIE (`converter.is_curie()`) or a URI (`converter.is_uri()`) under its definition. + +=== "Python" + + ```python + from curies_rs import get_obo_converter + + converter = get_obo_converter() + + assert converter.is_curie("GO:1234567") + assert not converter.is_curie("http://purl.obolibrary.org/obo/GO_1234567") + # This is a valid CURIE, but not under this converter's definition + assert not converter.is_curie("pdb:2gc4") + + assert converter.is_uri("http://purl.obolibrary.org/obo/GO_1234567") + assert not converter.is_uri("GO:1234567") + # This is a valid URI, but not under this converter's definition + assert not converter.is_uri("http://proteopedia.org/wiki/index.php/2gc4") + ``` + +=== "JavaScript" + + ```javascript + import {getOboConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getOboConverter(); + + console.log(converter.isCurie("GO:1234567")) + + console.log(converter.isUri("http://purl.obolibrary.org/obo/GO_1234567")) + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_obo_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_obo_converter().await?; + + assert_eq!(converter.is_curie("GO:1234567"), true); + + assert_eq!(converter.is_uri("http://purl.obolibrary.org/obo/GO_1234567"), true); + Ok(()) + } + ``` + +### ๐Ÿ—œ๏ธ Standardized Expansion and Compression + +The `converter.expand_or_standardize()` function extends the CURIE expansion function to handle the situation where you might get passed a CURIE or a URI. If itโ€™s a CURIE, expansions happen with the normal rules. If itโ€™s a URI, it tries to standardize it. + +=== "Python" + + ```python + from curies_rs import Converter + + converter = Converter.from_extended_prefix_map("""[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]""") + + # Expand CURIEs + assert converter.expand_or_standardize("CHEBI:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' + assert converter.expand_or_standardize("chebi:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' + + # standardize URIs + assert converter.expand_or_standardize("http://purl.obolibrary.org/obo/CHEBI_138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' + assert converter.expand_or_standardize("https://identifiers.org/chebi:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' + + # Handle cases that aren't valid w.r.t. the converter + try: + converter.expand_or_standardize("missing:0000000") + converter.expand_or_standardize("https://example.com/missing:0000000") + except Exception as e: + print(e) + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromExtendedPrefixMap(`[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]`) + + console.log(converter.expandOrStandardize("chebi:138488")) + console.log(converter.expandOrStandardize("https://identifiers.org/chebi:138488")) + try { + console.log(converter.expandOrStandardize("http://purl.obolibrary.org/UNKNOWN_12345")) + } catch (e) { + console.log("Failed successfully) + } + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_extended_prefix_map(r#"[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]"#).await?; + + assert_eq!(converter.expand_or_standardize("http://amigo.geneontology.org/amigo/term/GO:0032571").unwrap(), "http://purl.obolibrary.org/obo/GO_0032571".to_string()); + assert_eq!(converter.expand_or_standardize("gomf:0032571").unwrap(), "http://purl.obolibrary.org/obo/GO_0032571".to_string()); + assert!(converter.expand_or_standardize("http://purl.obolibrary.org/UNKNOWN_12345").is_err()); + Ok(()) + } + ``` + +A similar workflow is implemented in `converter.compress_or_standardize()` for compressing URIs where a CURIE might get passed. + +=== "Python" + + ```python + from curies_rs import Converter + + converter = Converter.from_extended_prefix_map("""[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]""") + + # Compress URIs + assert converter.compress_or_standardize("http://purl.obolibrary.org/obo/CHEBI_138488") == 'CHEBI:138488' + assert converter.compress_or_standardize("https://identifiers.org/chebi:138488") == 'CHEBI:138488' + + # standardize CURIEs + assert converter.compress_or_standardize("CHEBI:138488") == 'CHEBI:138488' + assert converter.compress_or_standardize("chebi:138488") == 'CHEBI:138488' + + # Handle cases that aren't valid w.r.t. the converter + try: + converter.compress_or_standardize("missing:0000000") + converter.compress_or_standardize("https://example.com/missing:0000000") + except Exception as e: + print(e) + print(type(e)) + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromExtendedPrefixMap(`[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]`) + + console.log(converter.compressOrStandardize("https://identifiers.org/chebi:138488")) + console.log(converter.compressOrStandardize("gomf:0032571")) + try { + console.log(converter.compressOrStandardize("http://purl.obolibrary.org/UNKNOWN_12345")) + } catch (e) { + console.log("Failed successfully) + } + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_extended_prefix_map(r#"[{ + "prefix": "CHEBI", + "prefix_synonyms": ["chebi"], + "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", + "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] + }]"#).await?; + + assert_eq!(converter.compress_or_standardize("http://amigo.geneontology.org/amigo/term/GO:0032571").unwrap(), "go:0032571".to_string()); + assert_eq!(converter.compress_or_standardize("gomf:0032571").unwrap(), "go:0032571".to_string()); + assert!(converter.compress_or_standardize("http://purl.obolibrary.org/UNKNOWN_12345").is_err()); + Ok(()) + } + ``` + +## ๐Ÿšš Bulk operations + +You can use the `expand_list()` and `compress_list()` functions to processes many URIs or CURIEs at once.. + +For example to create a new `URI` column in a pandas dataframe from a `CURIE` column: + +```python +import pandas as pd +from curies_rs import get_bioregistry_converter + +converter = get_bioregistry_converter() +df = pd.DataFrame({'CURIE': ['doid:1234', 'doid:5678', 'doid:91011']}) + +# Expand the list of CURIEs to URIs +df['URI'] = converter.expand_list(df['CURIE']) +print(df) +``` + +## ๐Ÿงฉ Integrating with [`rdflib`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#module-rdflib) + +RDFlib is a pure Python package for manipulating RDF data. The following example shows how to bind the extended prefix map from a `Converter` to a graph ([`rdflib.Graph`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.Graph)). + +```python +import curies_rs, rdflib, rdflib.namespace, json + +converter = curies_rs.get_obo_converter() +g = rdflib.Graph() + +for prefix, uri_prefix in json.loads(converter.write_prefix_map()).items(): + g.bind(prefix, rdflib.Namespace(uri_prefix)) +``` + +A more flexible approach is to instantiate a namespace manager ([`rdflib.namespace.NamespaceManager`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.namespace.html#rdflib.namespace.NamespaceManager)) and bind directly to that. + +```python +import curies_rs, rdflib, json + +converter = curies_rs.get_obo_converter() +namespace_manager = rdflib.namespace.NamespaceManager(rdflib.Graph()) + +for prefix, uri_prefix in json.loads(converter.write_prefix_map()).items(): + namespace_manager.bind(prefix, rdflib.Namespace(uri_prefix)) +``` + +URI references for use in RDFLibโ€™s graph class can be constructed from CURIEs using a combination of `converter.expand()` and [`rdflib.URIRef`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.URIRef). + +```python +import curies_rs, rdflib + +converter = curies_rs.get_obo_converter() + +uri_ref = rdflib.URIRef(converter.expand("CHEBI:138488")) +``` + + diff --git a/lib/docs/docs/getting-started.md b/lib/docs/docs/getting-started.md new file mode 100644 index 0000000..7a32244 --- /dev/null +++ b/lib/docs/docs/getting-started.md @@ -0,0 +1,893 @@ +# ๐Ÿš€ Getting started + +You can easily work with `curies` from various languages. + +## ๐Ÿ“ฅ๏ธ Installation + +Install the package for you language: + +=== "Python" + + ```bash + pip install curies-rs + ``` + +=== "JavaScript" + + ```bash + npm install --save @biopragmatics/curies + # or + pnpm add @biopragmatics/curies + # or + yarn add @biopragmatics/curies + # or + bun add @biopragmatics/curies + ``` + +=== "Rust" + + ```bash + cargo add curies + ``` + +## ๐Ÿš€ Usage + +Initialize a converter, then use it to `compress` URIs to CURIEs, or `expand` CURIEs to URIs: + +=== "Python" + + ```python + from curies_rs import get_bioregistry_converter + + # Initialize converter (here we use the predefined Bioregistry converter) + converter = get_bioregistry_converter() + + # Compress a URI, or expand a CURIE + curie = converter.compress("http://purl.obolibrary.org/obo/DOID_1234") + uri = converter.expand("DOID:1234") + + # Compress/expand a list of URIs or CURIEs + curies = converter.compress_list(["http://purl.obolibrary.org/obo/DOID_1234"]) + uris = converter.expand_list(["DOID:1234", "doid:1235"]) + + # Standardize prefix, CURIEs, and URIs using the preferred alternative + assert converter.standardize_prefix("gomf") == "go" + assert converter.standardize_curie("gomf:0032571") == "go:0032571aaaaaaa" + assert converter.standardize_uri("http://amigo.geneontology.org/amigo/term/GO:0032571") == "http://purl.obolibrary.org/obo/GO_0032571" + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + // Initialize converter (here we use the predefined Bioregistry converter) + const converter = await getBioregistryConverter(); + + // Compress a URI, or expand a CURIE + const curie = converter.compress("http://purl.obolibrary.org/obo/DOID_1234"); + const uri = converter.expand("doid:1234"); + + // Compress/expand a list of URIs or CURIEs + const curies = converter.compressList(["http://purl.obolibrary.org/obo/DOID_1234"]); + const uris = converter.expandList(["doid:1234"]); + + // Standardize prefix, CURIEs, and URIs using the preferred alternative + console.log(converter.standardizePrefix("gomf")) + console.log(converter.standardizeCurie("gomf:0032571")) + console.log(converter.standardizeUri("http://amigo.geneontology.org/amigo/term/GO:0032571")) + } + main(); + ``` + + !!! warning "Running in the browser requires initialization" + + When writing code that will be executed in the browser you need to first initialize the Wasm binary: + + ```javascript + import init, { Record, Converter, getOboConverter } from "@biopragmatics/curies"; + + async function main() { + await init(); + const converter = await getOboConverter(); + const uri = converter.expand("DOID:1234"); + } + main(); + ``` + + !!! danger "CORS exists" + + When executing JS in the browser we are bound to the same rules as everyone on the web, such as CORS. If CORS are not enabled on the server you are fetching the converter from, then you will need to use a proxy such as [corsproxy.io](https://corsproxy.io). + + !!! bug "Use HTTPS when importing" + + When loading converters from URLs in JS always prefer using HTTPS URLs, otherwise you will face `Mixed Content` errors. + + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + // Initialize converter (here we use the predefined Bioregistry converter) + let converter = get_bioregistry_converter().await?; + + // Compress a URI, or expand a CURIE + let uri = converter.expand("doid:1234")?; + let curie = converter.compress("http://purl.obolibrary.org/obo/DOID_1234")?; + + // Compress/expand a list of URIs or CURIEs + let uris = converter.expand_list(vec!["doid:1234"]); + let curies = converter.compress_list(vec!["http://purl.obolibrary.org/obo/DOID_1234"]); + + // Standardize prefix, CURIEs, and URIs using the preferred alternative + assert_eq!(converter.standardize_prefix("gomf").unwrap(), "go"); + assert_eq!(converter.standardize_curie("gomf:0032571").unwrap(), "go:0032571"); + assert_eq!(converter.standardize_uri( + "http://amigo.geneontology.org/amigo/term/GO:0032571").unwrap(), + "http://purl.obolibrary.org/obo/GO_0032571", + ); + Ok(()) + } + ``` + +## ๐ŸŒ€ Loading a Context + +There are several ways to load a context with this package, including: + +1. pre-defined contexts +2. contexts encoded in the standard prefix map format +3. contexts encoded in the standard JSON-LD context format +4. contexts encoded in the extended prefix map format + +### ๐Ÿ“ฆ Loading a predefined context + +Easiest way to get started is to simply use one of the function available to import a converter from popular namespaces registries: + +**[Bioregistry](https://bioregistry.io/) converter** + +=== "Python" + + ```python + from curies_rs import get_bioregistry_converter + + converter = get_bioregistry_converter() + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getBioregistryConverter(); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; + Ok(()) + } + ``` + +**[OBO](http://obofoundry.org/) converter** + +=== "Python" + + ```python + from curies_rs import get_obo_converter + + converter = get_obo_converter() + ``` + +=== "JavaScript" + + ```javascript + import {getOboConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getOboConverter(); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_obo_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_obo_converter().await?; + Ok(()) + } + ``` + + +**[GO](https://geneontology.org/) converter** + +=== "Python" + + ```python + from curies_rs import get_go_converter + + converter = get_go_converter() + ``` + +=== "JavaScript" + + ```javascript + import {getGoConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getGoConverter(); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_go_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_go_converter().await?; + Ok(()) + } + ``` + + +**[Monarch Initiative](https://monarchinitiative.org/) converter** + +=== "Python" + + ```python + from curies_rs import get_monarch_converter + + converter = get_monarch_converter() + ``` + +=== "JavaScript" + + ```javascript + import {getMonarchConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getMonarchConverter(); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_monarch_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_monarch_converter().await?; + Ok(()) + } + ``` + +### ๐Ÿ—บ๏ธ Loading Extended Prefix Maps + +Enable to provide prefix/URI synonyms and ID RegEx pattern for each record: + +=== "Python" + + ```python + from curies_rs import Converter + + extended_pm = """[ + { + "prefix": "DOID", + "prefix_synonyms": [ + "doid" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/DOID_", + "uri_prefix_synonyms": [ + "http://bioregistry.io/DOID:" + ], + "pattern": "^\\\\d+$" + }, + { + "prefix": "OBO", + "prefix_synonyms": [ + "obo" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/" + } + ]""" + converter = Converter.from_extended_prefix_map(extended_pm) + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromExtendedPrefixMap(`[ + { + "prefix": "DOID", + "prefix_synonyms": [ + "doid" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/DOID_", + "uri_prefix_synonyms": [ + "http://bioregistry.io/DOID:" + ], + "pattern": "^\\\\d+$" + }, + { + "prefix": "OBO", + "prefix_synonyms": [ + "obo" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/" + } + ]`) + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_extended_prefix_map(r#"[ + { + "prefix": "DOID", + "prefix_synonyms": [ + "doid" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/DOID_", + "uri_prefix_synonyms": [ + "http://bioregistry.io/DOID:" + ], + "pattern": "^\\\\d+$" + }, + { + "prefix": "OBO", + "prefix_synonyms": [ + "obo" + ], + "uri_prefix": "http://purl.obolibrary.org/obo/" + } + ]"#).await?; + Ok(()) + } + ``` + +!!! tip "Support URL" + + For all `Converter.from` functions you can either provide the file content, or the URL to the file as string. + +### ๐Ÿ“ Loading Prefix Maps + +A simple dictionary without synonyms information: + +=== "Python" + + ```python + from curies_rs import Converter + + prefix_map = """{ + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + }""" + converter = Converter.from_prefix_map(prefix_map) + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromPrefixMap(`{ + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + }`) + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_prefix_map(r#"{ + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + }"#).await?; + Ok(()) + } + ``` + +### ๐Ÿ“„ Loading JSON-LD contexts + +=== "Python" + + ```python + from curies_rs import Converter + + jsonld = """{ + "@context": { + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + } + }""" + converter = Converter.from_jsonld(jsonld) + ``` + + Or directly use a URL: + + ```python + from curies_rs import Converter + + converter = Converter.from_jsonld("https://purl.obolibrary.org/meta/obo_context.jsonld") + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromJsonld(`{ + "@context": { + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + } + }`) + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_jsonld(r#"{ + "@context": { + "GO": "http://purl.obolibrary.org/obo/GO_", + "DOID": "http://purl.obolibrary.org/obo/DOID_", + "OBO": "http://purl.obolibrary.org/obo/" + } + }"#).await?; + Ok(()) + } + ``` + +### ๐Ÿ”— Loading SHACL prefixes definitions + +=== "Python" + + ```python + from curies_rs import Converter + + shacl = """@prefix sh: . + @prefix xsd: . + [ + sh:declare + [ sh:prefix "dc" ; sh:namespace "http://purl.org/dc/elements/1.1/"^^xsd:anyURI ], + [ sh:prefix "dcterms" ; sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ], + [ sh:prefix "foaf" ; sh:namespace "http://xmlns.com/foaf/0.1/"^^xsd:anyURI ], + [ sh:prefix "xsd" ; sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] + ] .""" + conv = Converter.from_shacl(shacl) + ``` + +=== "JavaScript" + + ```javascript + import {Converter} from "@biopragmatics/curies"; + + async function main() { + const converter = await Converter.fromShacl(`@prefix sh: . + @prefix xsd: . + [ + sh:declare + [ sh:prefix "dc" ; sh:namespace "http://purl.org/dc/elements/1.1/"^^xsd:anyURI ], + [ sh:prefix "dcterms" ; sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ], + [ sh:prefix "foaf" ; sh:namespace "http://xmlns.com/foaf/0.1/"^^xsd:anyURI ], + [ sh:prefix "xsd" ; sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] + ] .`) + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::from_shacl(r#"@prefix sh: . + @prefix xsd: . + [ + sh:declare + [ sh:prefix "dc" ; sh:namespace "http://purl.org/dc/elements/1.1/"^^xsd:anyURI ], + [ sh:prefix "dcterms" ; sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ], + [ sh:prefix "foaf" ; sh:namespace "http://xmlns.com/foaf/0.1/"^^xsd:anyURI ], + [ sh:prefix "xsd" ; sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ] + ] ."#).await?; + Ok(()) + } + ``` + +## ๐Ÿ”Ž Introspecting on a Context + +After loading a context, itโ€™s possible to get certain information out of the converter. For example, if you want to get all of the CURIE prefixes from the converter, you can use `converter.get_prefixes()`: + +=== "Python" + + ```python + from curies_rs import get_bioregistry_converter + + converter = get_bioregistry_converter() + + prefixes = converter.get_prefixes() + assert 'chebi' in prefixes + assert 'CHEBIID' not in prefixes, "No synonyms are included by default" + + prefixes = converter.get_prefixes(include_synonyms=True) + assert 'chebi' in prefixes + assert 'CHEBIID' in prefixes + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getBioregistryConverter(); + + const prefixes = converter.getPrefixes(); + // Synonyms are not included by default + const prefixes_incl_syn = converter.getPrefixes(true); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; + + // Argument to include or not synonyms + let prefixes = converter.get_prefixes(false) + let prefixes_incl_syn = converter.get_prefixes(true) + Ok(()) + } + ``` + +Similarly, the URI prefixes can be extracted with `Converter.get_uri_prefixes()` like in: + +=== "Python" + + ```python + from curies_rs import get_bioregistry_converter + + converter = get_bioregistry_converter() + + uri_prefixes = converter.get_uri_prefixes() + assert 'http://purl.obolibrary.org/obo/CHEBI_' in uri_prefixes + assert 'https://bioregistry.io/chebi:' not in uri_prefixes, "No synonyms are included by default" + + uri_prefixes = converter.get_uri_prefixes(include_synonyms=True) + assert 'http://purl.obolibrary.org/obo/CHEBI_' in uri_prefixes + assert 'https://bioregistry.io/chebi:' in uri_prefixes + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getBioregistryConverter(); + + const prefixes = converter.getUriPrefixes(); + // Synonyms are not included by default + const prefixes_incl_syn = converter.getUriPrefixes(true); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; + + // Argument to include or not synonyms + let prefixes = converter.get_uri_prefixes(false) + let prefixes_incl_syn = converter.get_uri_prefixes(true) + Ok(()) + } + ``` + +Itโ€™s also possible to get a bijective prefix map, i.e., a dictionary from primary CURIE prefixes to primary URI prefixes. This is useful for compatibility with legacy systems which assume simple prefix maps. This can be done with the `write_prefix_map()` function like in the following: + +=== "Python" + + ```python + import json + from curies_rs import get_bioregistry_converter + + converter = get_bioregistry_converter() + + prefix_map = json.loads(converter.write_prefix_map()) + assert prefix_map['chebi'] == 'http://purl.obolibrary.org/obo/CHEBI_' + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getBioregistryConverter(); + + prefix_map = JSON.parse(converter.writePrefixMap()); + console.log(prefix_map['chebi']); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; + + let prefix_map = converter.write_prefix_map(); + // This returns a HashMap + match prefix_map.get("chebi") { + Some(value) => println!("{}", value), + None => println!("Key not found"), + } + Ok(()) + } + ``` + +## ๐Ÿ› ๏ธ Modifying a Context + +### ๐Ÿ”จ Incremental Converters + +New data can be added to an existing converter with either `converter.add_prefix()` or `converter.add_record()`. For example, a CURIE and URI prefix for HGNC can be added to the OBO Foundry converter with the following: + +=== "Python" + + ```python + from curies_rs import get_obo_converter + + converter = get_obo_converter() + converter.add_prefix("hgnc", "https://bioregistry.io/hgnc:") + ``` + +=== "JavaScript" + + ```javascript + import {Converter, Record} from "@biopragmatics/curies"; + + async function main() { + // Populate from Records + const rec1 = new Record("obo", "http://purl.obolibrary.org/obo/", [], []); + + console.log(rec1.toString()); + console.log(rec1.toJs()); + const converter = new Converter(); + converter.addRecord(rec1); + converter.addPrefix("hgnc", "https://bioregistry.io/hgnc:"); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_obo_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let mut converter = get_obo_converter().await?; + + converter.add_prefix("hgnc", "https://bioregistry.io/hgnc:")?; + Ok(()) + } + ``` + +Alternatively you can construct a `Record` object, which allows to pass synonyms lists, and start from a blank `Converter`: + +=== "Python" + + ```python + from curies_rs import Converter, Record + + converter = Converter() + record = Record( + prefix="hgnc", + uri_prefix="https://bioregistry.io/hgnc:", + prefix_synonyms=["HGNC"], + uri_prefix_synonyms=["https://identifiers.org/hgnc/"], + ) + converter.add_record(record) + ``` + +=== "JavaScript" + + ```javascript + import {Converter, Record} from "@biopragmatics/curies"; + + async function main() { + const converter = new Converter(); + const record = new Record("hgnc", "https://bioregistry.io/hgnc:", ["HGNC"], ["https://identifiers.org/hgnc/"]); + converter.addRecord(record); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::{Converter, Record}; + use std::collections::HashSet; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let mut converter = Converter::default(); + + let record = Record { + prefix: "hgnc".to_string(), + uri_prefix: "https://bioregistry.io/hgnc:".to_string(), + prefix_synonyms: HashSet::from(["HGNC".to_string()]), + uri_prefix_synonyms: HashSet::from(["https://identifiers.org/hgnc/"].map(String::from)), + pattern: None, + }; + converter.add_record(record)?; + Ok(()) + } + ``` + +By default, both of these operations will fail if the new content conflicts with existing content. If desired, the `merge` argument can be set to true to enable merging. Further, checking for conflicts and merging can be made to be case insensitive by setting `case_sensitive` to false. + +Such a merging strategy is the basis for wholesale merging of converters, described below. + +### โ›“๏ธ Chaining and merging + +Chain together multiple converters, prioritizes based on the order given. Therefore, if two prefix maps having the same prefix but different URI prefixes are given, the first is retained. The second is retained as a synonym + +=== "Python" + + ```python + from curies_rs import get_obo_converter, get_go_converter, get_monarch_converter + + converter = ( + get_obo_converter() + .chain(get_go_converter()) + .chain(get_monarch_converter()) + ) + ``` + +=== "JavaScript" + + ```javascript + import {getOboConverter, getGoConverter, getMonarchConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getOboConverter() + .chain(await getGoConverter()) + .chain(await getMonarchConverter()); + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::Converter; + use curies::sources::{get_obo_converter, get_go_converter, get_monarch_converter}; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = Converter::chain(vec![ + get_obo_converter().await?, + get_go_converter().await?, + get_monarch_converter().await?, + ])?;; + Ok(()) + } + ``` + + + +## โœ’๏ธ Writing a Context + +Write the converter prefix map as a string in different serialization format: + +=== "Python" + + ```python + from curies_rs import get_bioregistry_converter + + converter = get_bioregistry_converter() + + epm = converter.write_extended_prefix_map() + pm = converter.write_prefix_map() + jsonld = converter.write_jsonld() + shacl = converter.write_shacl() + ``` + +=== "JavaScript" + + ```javascript + import {getBioregistryConverter} from "@biopragmatics/curies"; + + async function main() { + const converter = await getBioregistryConverter(); + + const epm = converter.writeExtendedPrefixMap() + const pm = converter.writePrefixMap() + const jsonld = converter.writeJsonld() + const shacl = converter.writeShacl() + } + main(); + ``` + +=== "Rust" + + ```rust + use curies::sources::get_bioregistry_converter; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; + + let epm = converter.write_extended_prefix_map()?; + let pm = converter.write_prefix_map(); + let jsonld = converter.write_jsonld(); + let shacl = converter.write_shacl()?; + Ok(()) + } + ``` diff --git a/lib/docs/docs/index.md b/lib/docs/docs/index.md index 813a91e..674066e 100644 --- a/lib/docs/docs/index.md +++ b/lib/docs/docs/index.md @@ -1,4 +1,4 @@ -# Introduction +# โ„น๏ธ Introduction [![crates.io](https://img.shields.io/crates/v/curies.svg)](https://crates.io/crates/curies) [![PyPI](https://img.shields.io/pypi/v/curies-rs)](https://pypi.org/project/curies-rs/) diff --git a/lib/docs/docs/javascript-example-framework.md b/lib/docs/docs/javascript-example-framework.md index 4013073..937dc6f 100644 --- a/lib/docs/docs/javascript-example-framework.md +++ b/lib/docs/docs/javascript-example-framework.md @@ -1,6 +1,6 @@ # โš›๏ธ Use from any JavaScript framework -It can be used from any JavaScript framework, or NodeJS. +This package can be used from any JavaScript framework, or NodeJS. For example, to use it in a nextjs react app: diff --git a/lib/docs/docs/javascript-example-html.md b/lib/docs/docs/javascript-example-html.md index 31e4131..5db07c4 100644 --- a/lib/docs/docs/javascript-example-html.md +++ b/lib/docs/docs/javascript-example-html.md @@ -1,4 +1,4 @@ -# ๐Ÿš€ Example in bare HTML files +# ๐Ÿ“„ Example in bare HTML files When using the library directly in the client browser you will need to initialize the wasm binary with `await init()`, after that you can use the same functions as in the NodeJS environments. diff --git a/lib/docs/docs/python-devtools.md b/lib/docs/docs/python-devtools.md deleted file mode 100644 index cf1a2db..0000000 --- a/lib/docs/docs/python-devtools.md +++ /dev/null @@ -1,140 +0,0 @@ -# ๐Ÿงฐ Tools for Developers and Semantic Engineers - -## ๐Ÿช„ Working with strings that might be a URI or a CURIE - -Sometimes, itโ€™s not clear if a string is a CURIE or a URI. While the [SafeCURIE syntax](https://www.w3.org/TR/2010/NOTE-curie-20101216/#P_safe_curie) is intended to address this, itโ€™s often overlooked. - -### โ˜‘๏ธ CURIE and URI Checks - -The first way to handle this ambiguity is to be able to check if the string is a CURIE or a URI. Therefore, each `Converter` comes with functions for checking if a string is a CURIE (`converter.is_curie()`) or a URI (`converter.is_uri()`) under its definition. - -```python -from curies_rs import get_obo_converter - -converter = get_obo_converter() - -assert converter.is_curie("GO:1234567") -assert not converter.is_curie("http://purl.obolibrary.org/obo/GO_1234567") -# This is a valid CURIE, but not under this converter's definition -assert not converter.is_curie("pdb:2gc4") - -assert converter.is_uri("http://purl.obolibrary.org/obo/GO_1234567") -assert not converter.is_uri("GO:1234567") -# This is a valid URI, but not under this converter's definition -assert not converter.is_uri("http://proteopedia.org/wiki/index.php/2gc4") -``` - -### ๐Ÿ—œ๏ธ Standardized Expansion and Compression - -The `converter.expand_or_standardize()` function extends the CURIE expansion function to handle the situation where you might get passed a CURIE or a URI. If itโ€™s a CURIE, expansions happen with the normal rules. If itโ€™s a URI, it tries to standardize it. - -```python -from curies_rs import Converter - -converter = Converter.from_extended_prefix_map("""[{ - "prefix": "CHEBI", - "prefix_synonyms": ["chebi"], - "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", - "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] -}]""") - -# Expand CURIEs -assert converter.expand_or_standardize("CHEBI:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' -assert converter.expand_or_standardize("chebi:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' - -# standardize URIs -assert converter.expand_or_standardize("http://purl.obolibrary.org/obo/CHEBI_138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' -assert converter.expand_or_standardize("https://identifiers.org/chebi:138488") == 'http://purl.obolibrary.org/obo/CHEBI_138488' - -# Handle cases that aren't valid w.r.t. the converter -try: - converter.expand_or_standardize("missing:0000000") - converter.expand_or_standardize("https://example.com/missing:0000000") -except Exception as e: - print(e) -``` - -A similar workflow is implemented in `converter.compress_or_standardize()` for compressing URIs where a CURIE might get passed. - -```python -from curies_rs import Converter - -converter = Converter.from_extended_prefix_map("""[{ - "prefix": "CHEBI", - "prefix_synonyms": ["chebi"], - "uri_prefix": "http://purl.obolibrary.org/obo/CHEBI_", - "uri_prefix_synonyms": ["https://identifiers.org/chebi:"] -}]""") - -# Compress URIs -assert converter.compress_or_standardize("http://purl.obolibrary.org/obo/CHEBI_138488") == 'CHEBI:138488' -assert converter.compress_or_standardize("https://identifiers.org/chebi:138488") == 'CHEBI:138488' - -# standardize CURIEs -assert converter.compress_or_standardize("CHEBI:138488") == 'CHEBI:138488' -assert converter.compress_or_standardize("chebi:138488") == 'CHEBI:138488' - -# Handle cases that aren't valid w.r.t. the converter -try: - converter.compress_or_standardize("missing:0000000") - converter.compress_or_standardize("https://example.com/missing:0000000") -except Exception as e: - print(e) - print(type(e)) -``` - -## ๐Ÿšš Bulk operations - -You can use the `expand_list()` and `compress_list()` functions to processes many URIs or CURIEs at once.. - -For example to create a new `URI` column in a pandas dataframe from a `CURIE` column: - -```python -import pandas as pd -from curies_rs import get_bioregistry_converter - -converter = get_bioregistry_converter() -df = pd.DataFrame({'CURIE': ['doid:1234', 'doid:5678', 'doid:91011']}) - -# Expand the list of CURIEs to URIs -df['URI'] = converter.expand_list(df['CURIE']) -print(df) -``` - -## ๐Ÿงฉ Integrating with [`rdflib`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#module-rdflib) - -RDFlib is a pure Python package for manipulating RDF data. The following example shows how to bind the extended prefix map from a `Converter` to a graph ([`rdflib.Graph`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.Graph)). - -```python -import curies_rs, rdflib, rdflib.namespace, json - -converter = curies_rs.get_obo_converter() -g = rdflib.Graph() - -for prefix, uri_prefix in json.loads(converter.write_prefix_map()).items(): - g.bind(prefix, rdflib.Namespace(uri_prefix)) -``` - -A more flexible approach is to instantiate a namespace manager ([`rdflib.namespace.NamespaceManager`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.namespace.html#rdflib.namespace.NamespaceManager)) and bind directly to that. - -```python -import curies_rs, rdflib, json - -converter = curies_rs.get_obo_converter() -namespace_manager = rdflib.namespace.NamespaceManager(rdflib.Graph()) - -for prefix, uri_prefix in json.loads(converter.write_prefix_map()).items(): - namespace_manager.bind(prefix, rdflib.Namespace(uri_prefix)) -``` - -URI references for use in RDFLibโ€™s graph class can be constructed from CURIEs using a combination of `converter.expand()` and [`rdflib.URIRef`](https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.URIRef). - -```python -import curies_rs, rdflib - -converter = curies_rs.get_obo_converter() - -uri_ref = rdflib.URIRef(converter.expand("CHEBI:138488")) -``` - - diff --git a/lib/docs/docs/rust.md b/lib/docs/docs/rust.md index 1c76e3d..bcbd885 100644 --- a/lib/docs/docs/rust.md +++ b/lib/docs/docs/rust.md @@ -13,11 +13,11 @@ cargo add curies You can use the Rust crate to work with CURIEs: import converters, compress URIs, expand CURIEs. ```rust -extern crate curies; use curies::{Converter, Record, sources::get_bioregistry_converter}; use std::collections::HashSet; -async fn usage_example() -> Result<(), Box> { +#[tokio::main] +async fn main() -> Result<(), Box> { // Load from a prefix map json (string or URI) let converterFromMap = Converter::from_prefix_map(r#"{ @@ -43,11 +43,23 @@ async fn usage_example() -> Result<(), Box> { println!("Compressed URI: {}", curie); Ok(()) } +``` + +And + +```rust +use curies::sources::get_bioregistry_converter; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let converter = get_bioregistry_converter().await?; -let rt = tokio::runtime::Runtime::new().unwrap(); -rt.block_on(async { - usage_example().await -}).unwrap(); + let epm = converter.write_extended_prefix_map()?; + let pm = converter.write_prefix_map(); + let jsonld = converter.write_jsonld(); + let shacl = converter.write_shacl()?; + Ok(()) +} ``` ## ๐Ÿ› ๏ธ Manipulate converters and records diff --git a/lib/docs/includes/abbreviations.md b/lib/docs/includes/abbreviations.md index 09c48fd..d727fff 100644 --- a/lib/docs/includes/abbreviations.md +++ b/lib/docs/includes/abbreviations.md @@ -49,6 +49,8 @@ *[CPU]: Central Processing Unit *[TPU]: Tensor Processing Unit *[aarch64]: ARM64 architecture +*[ARM]: Advanced RISC Machine (RISC: Reduced Instruction Set Computer) +*[x86]: Family of Complex Instruction Set Computer (CISC) architectures initially developed by Intel based on the Intel 8086 microprocessor *[wasm]: WebAssembly *[PDF]: Portable Document Format *[ZSH]: The Z shell (Zsh) is a Unix shell that can be used as an interactive login shell, and as a command interpreter for shell scripting. diff --git a/lib/docs/mkdocs.yml b/lib/docs/mkdocs.yml index c1a2980..2fd812b 100644 --- a/lib/docs/mkdocs.yml +++ b/lib/docs/mkdocs.yml @@ -10,23 +10,29 @@ copyright: Copyright © 2024 Charles Tapley Hoyt & Vincent Emonet # Find icons: https://fontawesome.com/icons/ # https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/ nav: - - Docs: - - Introduction: index.md - - Data structures: struct.md - # - Reconciliation: reconciliation.md - - Architecture details: architecture.md - - Contributing: contributing.md - - Rust: - - Use from Rust: rust.md - - Python: - - Use from Python: python.md - - Tools for Developers and Semantic Engineers: python-devtools.md + # - Docs: + - Introduction: index.md + - Getting started: getting-started.md + - Tools for Developers and Semantic Engineers: devtools.md + - Data structures: struct.md + # - Reconciliation: reconciliation.md - JavaScript: - - Use from JavaScript: javascript.md - Example bare HTML: javascript-example-html.md - Example JS framework: javascript-example-framework.md - - R: - - Use from R: r.md + - Development: + - Architecture details: architecture.md + - Contributing: contributing.md + # - Rust: + # - Use from Rust: rust.md + # - Python: + # - Use from Python: python.md + # - Tools for Developers and Semantic Engineers: python-devtools.md + # - JavaScript: + # - Use from JavaScript: javascript.md + # - Example bare HTML: javascript-example-html.md + # - Example JS framework: javascript-example-framework.md + # - R: + # - Use from R: r.md # - Issues: https://github.com/biopragmatics/curies.rs/issues" target="_blank theme: @@ -56,12 +62,13 @@ theme: features: - navigation.indexes - navigation.sections - - navigation.tabs + # - navigation.tabs - navigation.top - navigation.tracking - content.code.copy - content.code.annotate - content.code.select + - content.tabs.link # Group tabs switch - search.highlight - search.share - search.suggest @@ -94,6 +101,9 @@ markdown_extensions: - pymdownx.superfences - pymdownx.tabbed: alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower # slugify: !!python/object/apply:pymdownx.slugs.slugify # kwds: # case: lower diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 53f7781..a46aab4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -10,3 +10,8 @@ pub mod sources; pub use api::{Converter, Record}; pub use error::CuriesError; + +// NOTE: Add tests from markdown without putting it into docs +// #[doc = include_str!("../docs/docs/getting-started.md")] +// #[cfg(doctest)] +// pub struct _ReadmeDoctests;