Skip to content

Commit

Permalink
feat: Add functions compress_or_standardize, `expand_or_standardize…
Browse files Browse the repository at this point in the history
…`, `is_uri`, `is_curie`. Add git cliff to generate `CHANGELOG.md` and release notes
  • Loading branch information
vemonet committed Apr 19, 2024
1 parent ae67b97 commit b663c97
Show file tree
Hide file tree
Showing 24 changed files with 925 additions and 176 deletions.
62 changes: 42 additions & 20 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Build
on:
workflow_dispatch:
release:
types:
- published
push:
tags:
- "v*.*.*"
Expand All @@ -12,7 +9,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# cf. https://github.com/oxigraph/oxigraph/blob/main/.github/workflows/artifacts.yml
jobs:

npm_tarball:
Expand Down Expand Up @@ -46,20 +42,7 @@ jobs:
working-directory: ./js
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
if: github.event_name == 'release'

publish_crates:
name: 📦️ Publish crates to crates.io
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup update
- run: cargo login $CRATES_IO_TOKEN
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
- run: cargo publish
working-directory: ./lib
if: startsWith(github.ref, 'refs/tags/')

# Inspired by pydantic: https://github.com/pydantic/pydantic-core/blob/main/.github/workflows/ci.yml
build_wheels:
Expand Down Expand Up @@ -165,9 +148,48 @@ jobs:
path: python/dist

- name: Publish to PyPI
if: github.event_name == 'release'
# if: startsWith(github.ref, 'refs/tags/')
if: startsWith(github.ref, 'refs/tags/')
run: twine upload python/dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}


publish_crates:
name: 📦️ Publish crates to crates.io
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup update
- run: cargo login $CRATES_IO_TOKEN
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
- run: cargo publish
working-directory: ./lib

generate-changelog:
name: 🏔️ Generate changelog for GitHub release
runs-on: ubuntu-latest
outputs:
release_body: ${{ steps.git-cliff.outputs.content }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate a changelog
uses: orhun/git-cliff-action@main
id: git-cliff
with:
config: cliff.toml
args: -vv --latest --strip header
env:
OUTPUT: CHANGES.md

- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: CHANGES.md
if: startsWith(github.ref, 'refs/tags/')
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
RUST_BACKTRACE: 1

cov-rust:
name: ☂️ Coverage Rust
name: ☂️ Test with coverage Rust
runs-on: ubuntu-latest
container:
image: xd009642/tarpaulin:develop-nightly
Expand All @@ -42,10 +42,10 @@ jobs:
run: bash ./scripts/cov.sh

- name: Upload to codecov.io
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: false
# token: ${{secrets.CODECOV_TOKEN}}
token: ${{secrets.CODECOV_TOKEN}}
# fail_ci_if_error: false


test-js:
Expand Down
72 changes: 72 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[git]
# NOTE: Add an exclamation mark in your commit message prefix to indicate a BREAKING change
# e.g. `feat!: changed things` or `feat(python)!: changed things`
# More details about the standard at https://www.conventionalcommits.org
commit_parsers = [
{ message = "^feat", group = "⛰️ Features" },
{ message = "^fix", group = "🐛 Bug Fixes" },
{ message = "^doc", group = "📚 Documentation" },
{ message = "^perf|DataOriented", group = "⚡ Performance" },
{ message = "^refactor", group = "🚜 Refactor" },
{ message = "^style|Formatting", group = "🎨 Styling" },
{ message = "^test", group = "🧪 Testing" },
{ message = "^ci", group = "⚙️ Continuous Integration" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "🛠️ Miscellaneous Tasks" },
{ body = ".*security", group = "🛡️ Security" },
{ message = "^revert", group = "◀️ Revert" },
]
conventional_commits = true
filter_unconventional = false
split_commits = false
# Protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
filter_commits = false
tag_pattern = "v?[0-9].*"

# Sort the tags topologically
topo_order = false
sort_commits = "oldest"

# skip_tags = "0.1.0-beta.1" # regex for skipping tags
# ignore_tags = "" # regex for ignoring tags
# limit_commits = 42 # limit the number of commits included in the changelog.
# regex for preprocessing the commit messages:
# commit_preprocessors = [
# # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"}, # replace issue numbers
# ]


[changelog]
header = """
# 📜 Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# Template for the changelog: https://keats.github.io/tera/docs
body = """
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}](<REPO>/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}](<REPO>/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first | trim }} - ([{{ commit.id | truncate(length=7, end="") }}](<REPO>/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
trim = true
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<REPO>', replace = "https://github.com/vemonet/nanopub-rs" },
]
28 changes: 28 additions & 0 deletions js/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,34 @@ impl ConverterJs {
.map_err(|e| JsValue::from_str(&e.to_string()))
}

/// Checks if a given string is a valid URI according to the current `Converter`
#[wasm_bindgen(js_name = isUri)]
pub fn is_uri(&self, uri: String) -> bool {
self.converter.is_uri(&uri)
}

/// Checks if a given string is a valid CURIE according to the current `Converter`
#[wasm_bindgen(js_name = isCurie)]
pub fn is_curie(&self, curie: String) -> bool {
self.converter.is_curie(&curie)
}

/// Attempts to compress a URI to a CURIE, or standardize it if it's already a CURIE.
#[wasm_bindgen(js_name = compressOrStandardize)]
pub fn compress_or_standardize(&self, input: String) -> Result<String, JsValue> {
self.converter
.compress_or_standardize(&input)
.map_err(|e| JsValue::from_str(&e.to_string()))
}

/// Attempts to expand a CURIE to a URI, or standardize it if it's already a URI.
#[wasm_bindgen(js_name = expandOrStandardize)]
pub fn expand_or_standardize(&self, input: String) -> Result<String, JsValue> {
self.converter
.expand_or_standardize(&input)
.map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen(js_name = getPrefixes)]
pub fn get_prefixes(&self, include_synonyms: Option<bool>) -> Vec<String> {
self.converter
Expand Down
29 changes: 28 additions & 1 deletion js/tests/curies.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {describe, expect, test} from '@jest/globals';
import {Record, Converter, getOboConverter, getBioregistryConverter, getMonarchConverter, getGoConverter} from "../pkg/node";

// NOTE: `await init()` only needed in browser environment
describe('Tests for the curies npm package', () => {
// NOTE: `await init()` only needed in browser environment

test('from empty converter', async () => {
const converter = new Converter();
Expand Down Expand Up @@ -110,16 +110,43 @@ describe('Tests for the curies npm package', () => {
expect(converter.expand("doid:1234")).toBe("http://purl.obolibrary.org/obo/DOID_1234");
});

test('compress/expand or standardize', async () => {
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:"]
}]`);
expect(converter.expandOrStandardize("CHEBI:138488")).toBe("http://purl.obolibrary.org/obo/CHEBI_138488");
expect(converter.expandOrStandardize("chebi:138488")).toBe("http://purl.obolibrary.org/obo/CHEBI_138488");
expect(converter.expandOrStandardize("http://purl.obolibrary.org/obo/CHEBI_138488")).toBe("http://purl.obolibrary.org/obo/CHEBI_138488");
expect(converter.expandOrStandardize("https://identifiers.org/chebi:138488")).toBe("http://purl.obolibrary.org/obo/CHEBI_138488");

expect(converter.compressOrStandardize("http://purl.obolibrary.org/obo/CHEBI_138488")).toBe("CHEBI:138488");
expect(converter.compressOrStandardize("https://identifiers.org/chebi:138488")).toBe("CHEBI:138488");
expect(converter.compressOrStandardize("CHEBI:138488")).toBe("CHEBI:138488");
expect(converter.compressOrStandardize("chebi:138488")).toBe("CHEBI:138488");
});

test('get OBO converter', async () => {
const converter = await getOboConverter();
expect(converter.compress("http://purl.obolibrary.org/obo/DOID_1234")).toBe("DOID:1234");
expect(converter.expand("DOID:1234")).toBe("http://purl.obolibrary.org/obo/DOID_1234");

expect(converter.isCurie("GO:1234567")).toBe(true);
expect(converter.isCurie("http://purl.obolibrary.org/obo/GO_1234567")).toBe(false);
expect(converter.isCurie("pdb:2gc4")).toBe(false);

expect(converter.isUri("http://purl.obolibrary.org/obo/GO_1234567")).toBe(true);
expect(converter.isUri("GO:1234567")).toBe(false);
expect(converter.isUri("http://proteopedia.org/wiki/index.php/2gc4")).toBe(false);
});

test('get Bioregistry converter', async () => {
const converter = await getBioregistryConverter();
expect(converter.compress("http://purl.obolibrary.org/obo/DOID_1234")).toBe("doid:1234");
expect(converter.expand("doid:1234")).toBe("http://purl.obolibrary.org/obo/DOID_1234");

expect(converter.standardizePrefix("gomf")).toBe("go");
expect(converter.standardizeCurie("gomf:0032571")).toBe("go:0032571");
expect(converter.standardizeUri("http://amigo.geneontology.org/amigo/term/GO:0032571")).toBe("http://purl.obolibrary.org/obo/GO_0032571");
Expand Down
55 changes: 29 additions & 26 deletions lib/docs/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,35 @@ curies.rs/

List of features available per language binding, based on features defined in [curies.readthedocs.io](https://curies.readthedocs.io)

| Feature | Rust (core) | Python | JS | R |
| ------------------------------------------------ | ----------- | ------ | ---- | ---- |
| compress |||||
| expand |||||
| compress_list |||| |
| expand_list |||| |
| standardize (prefix, curie, uri) |||| |
| chain converters |||| |
| Record object and converter.add_record() |||| |
| converter.add_prefix(prefix, ns) |||| |
| converter.get_prefixes() and .get_uri_prefixes() |||| |
| Load from prefix map |||| |
| Load from extended prefix map |||| |
| Load from JSON-LD context |||| |
| Load from SHACL prefix definition |||| |
| Load OBO converter |||| |
| Load GO converter |||| |
| Load Bioregistry converter |||||
| Load Monarch converter |||| |
| Write converter to prefix map |||| |
| Write converter to extended prefix map |||| |
| Write converter to JSON-LD |||| |
| Write converter to SHACL |||| |
| Prefixes discovery | | | | |

## ⚠️​ Differences between rust core and language bindings
| Feature | Rust (core) | Python | JS | R |
| ----------------------------------------------------- | ----------- | ------ | ---- | ---- |
| compress |||||
| expand |||||
| compress_list |||| |
| expand_list |||| |
| standardize (prefix, curie, uri) |||| |
| is_uri() and is_curie() |||| |
| expand_or_standardize() and compress_or_standardize() |||| |
| chain converters |||| |
| Record object and converter.add_record() |||| |
| converter.add_prefix(prefix, ns) |||| |
| converter.get_prefixes() and .get_uri_prefixes() |||| |
| Load from prefix map |||| |
| Load from extended prefix map |||| |
| Load from JSON-LD context |||| |
| Load from SHACL prefix definition |||| |
| Load OBO converter |||| |
| Load GO converter |||| |
| Load Bioregistry converter |||||
| Load Monarch converter |||| |
| Write converter to prefix map |||| |
| Write converter to extended prefix map |||| |
| Write converter to JSON-LD |||| |
| Write converter to SHACL |||| |
| .get_subconverter() | | | | |
| Prefixes discovery | | | | |

## ⚠️​ Differences between Rust core and language bindings

1. The **functions to Load** prefix map, extended prefix map and JSON-LD can take `HashMap` as input in rust. But for JS and python, we currently need to pass it as `String` (we need to figure out how to pass arbitrary objects). You can pass either a URL or a JSON object as string, the lib will automatically retrieve the content of the URL if it is one. The original python lib was taking directly JSON objects for all loaders, apart from SHACL which takes a URL (which was not convenient when wanting to provide a local SHACL file)
2. In rust **chain()** is a static function taking a list of converters, `chained = Converter::chain([conv1, conv2])`. In JS and python we cannot easily pass a list of complex objects like converters, so chain is a normal function that takes 1 converter to chain: `chained = conv1.chain(conv2)`
Expand Down
25 changes: 15 additions & 10 deletions lib/docs/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Install development dependencies:
./scripts/install-dev.sh
```

> If you are using VSCode we strongly recommend to install the [`rust-lang.rust-analyzer`](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension.
!!! tip "VSCode extension"

If you are using VSCode we strongly recommend to install the [`rust-lang.rust-analyzer`](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension.

## 🧪 Run tests

Expand Down Expand Up @@ -116,7 +118,10 @@ The first time you will need to add the `--install` flag to install dependencies
./scripts/test-r.sh --install
```

> You can force `rextendr` to re-build the bindings by making a change to one of the docstring `///` in the `/r/rust/src` code
!!! info "Force build"

You can force `rextendr` to re-build the bindings by making a change to one of the docstring `///` in the `/r/rust/src` code


## 🧹 Format and lint

Expand Down Expand Up @@ -155,16 +160,16 @@ cargo update
cargo outdated
```

## 🏷️ New release
## 🏷️ Publish a new release

Publishing artifacts will be done by the `build.yml` workflow, make sure you have set the following tokens as secrets for this repository: `PYPI_TOKEN`, `NPM_TOKEN`, `CRATES_IO_TOKEN`, `CODECOV_TOKEN`
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`

1. Bump the version in the `Cargo.toml` file in folders `lib`, `python`, and `js`:
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:

```bash
./scripts/bump.sh 0.1.2
```
```bash
./scripts/release.sh 0.1.2
```

2. Commit, push, and **create a new release on GitHub**.
!!! success "Automated release"

3. The `build.yml` workflow will automatically build artifacts (pip wheel, npm package), add them to the new release, and publish to public registries (crates.io, PyPI, NPM).
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.
Loading

0 comments on commit b663c97

Please sign in to comment.