diff --git a/.ci/mknotes b/.ci/mknotes deleted file mode 100755 index 70ca814..0000000 --- a/.ci/mknotes +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env perl - -package mknotes; - -use strict; -use warnings; -use v5.30; -use Getopt::Long qw(GetOptions); -use File::Path qw(make_path); -use File::Basename qw(dirname); -use utf8; - -my $item_regex = qr/^[*-+]\s+/; -my $indent_regex = qr/^\s+/; -my $version_regex = qr/^version\s*=\s*["']([^"']+)/; - -sub new { - my $self = { - cargo => 'Cargo.toml', - file => '', - repo => '', - output => '', - }; - - GetOptions( - "f=s" => \$self->{file}, - "c=s" => \$self->{cargo}, - "r=s" => \$self->{repo}, - "o=s" => \$self->{output}, - ) or die("Error in command line arguments\n"); - - if (!$self->{file} || !$self->{repo}) { - die "Usage: mknotes -f -r [-c ] [ -o ]\n"; - } - - # Trim a slash from the URI. - $self->{repo} =~ s{/$}{}; - - bless $self => __PACKAGE__; -} - -sub run { - my $self = shift; - my $version = $self->parse_version; - - open my $fh, '<:encoding(UTF-8)', $self->{file} - or die "Cannot open $self->{file}: $!\n"; - - # Print to STDOUT by default. - my $out; - if ($self->{output}) { - my $dir = dirname $self->{output}; - make_path $dir unless -d $dir; - open $out, '>:encoding(UTF-8)', $self->{output} - or die "Cannot open $self->{output}: $!\n"; - } else { - $out = \*STDOUT; - binmode $out, ':encoding(UTF-8)'; - } - - - my $header = "## [v$self->{version}]"; - my ($found, $in_item) = (0, 0); - - while (my $line = <$fh>) { - if ($line =~ /^\Q$header/) { - # Found the header for this version. Build a regex for its link - # reference. - $found = 1; - my $link_regex = qr/^\s*\Q[v$self->{version}]\E:\s+https:/; - - # Continue scanning until we reach the next `## ` header. - while (my $line = <$fh>) { - chomp $line; - return $self->finish_list($out, $line) if $line =~ /^## /; - - # Skip the line if it's the link reference for the header. - $in_item = $self->print_line($out, $line, $in_item) - unless $line =~ $link_regex; - } - # Reach the end of the file; must be the first release. Diff the - # initial commit. - print {$out} "\n\n"; - return $self->finish_list($out, $self->initial_commit); - } - } - - # All done! - die "Version $self->{version} not found in $self->{file}\n" unless $found; -} - -# Called when no previous releases found. Return the initial commit in -# brackets to be parsed by finish_list(). -sub initial_commit { - my $sha = qx(git rev-list --max-parents=0 HEAD); - return "[" . substr($sha, 0, 7) . "]"; -} - -# Called when the next version header is found in line. -sub finish_list { - my ($self, $out, $line) = @_; - # Next header. Extract its version. - - my ($prev) = $line =~ /\[([^]]+)\]/; - die "No version found in $line\n" unless $prev; - - # Emit a footer line with a link to the diff for this version. - print {$out} sprintf( - "---\n\nšŸ†š For more detail compare [changes since %s](%s/compare/%s...v%s).\n", - $prev, $self->{repo}, $prev, $self->{version}, - ); - - # All done. - return 1; -} - -# Called for a line to print, keeping track of list item status. -sub print_line { - my ($self, $out, $line, $in_item) = @_; - # Convert wrapped list items to single lines. - if ($line =~ $item_regex) { - if ($in_item) { - # Previous item done - print {$out} "\n"; - } else { - # We're in an item now. - $in_item = 1 - } - # Print the line with no newline. - print {$out} $line; - - return $in_item; - } - - if ($in_item) { - # In an item, but not starting a new item. - if ($line =~ $indent_regex) { - # Continued item, convert indent to single space. - $line =~ s/$indent_regex/ /g; - # Print without newline. - print {$out} $line; - } else { - # No longer in an item. - $in_item = 0; - # Previous item done; print complete line. - print {$out} "\n", $line, "\n"; - } - - return $in_item; - } - - # Not in an item. - $in_item = 0; - # Print complete line - print {$out} $line, "\n"; - - return $in_item; -} - -sub parse_version { - my $self = shift; - open my $fh, '<:encoding(UTF-8)', $self->{cargo} - or die "Cannot open $self->{cargo}: $!\n"; - while (<$fh>) { - return $self->{version} = $1 if /$version_regex/; - } - die "Version field not found in $self->{cargo}\n"; -} - -package main; - -mknotes->new->run; - -__END__ diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 8abda83..c723153 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -210,14 +210,15 @@ jobs: exit 1 fi if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) - - name: Generate Release Changes - run: make target/release-notes.md - if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) + - name: Generate Release Notes + id: notes + uses: theory/changelog-version-notes-action@v0 + with: { version: "${{ env.VERSION }}" } - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: draft: true name: "Relelase ${{ env.VERSION }}" files: "pgxn_meta-*" - body_path: target/release-notes.md + body_path: ${{ steps.notes.outputs.file }} if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bd90e..ff6888e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,32 @@ All notable changes to this project will be documented in this file. It uses the [Semantic Versioning]: https://semver.org/spec/v2.0.0.html "Semantic Versioning 2.0.0" -## [Unreleased] ā€” Date TBD +## [v0.2.0] ā€” Date TBD +### āš” Improvements + +* Added the [meta module], which loads v1 and v2 spec files, converts v1 + metadata to v2, and merges multiple files. + +### šŸŖ² Bug Fixes + +* Changed the v1 validator to allow `http` as well as `https` in the + `meta-spec` object's `url` field, as a lot of older `META.json` files use + it. + +### šŸ“” Notes + +* Moved the validation functionality to the [valid module]. + +### šŸ“š Documentation + +* Updated the `v2` link in all docs to point to the [pull request], since it + hasn't been merged and published yet. + + [v0.2.0]: https://github.com/pgxn/meta/compare/v0.1.0...v0.2.0 + [meta module]: https://docs.rs/pgxn_meta/meta/ + [valid module]: https://docs.rs/pgxn_meta/meta/ + [pull request]: https://github.com/pgxn/rfcs/pull/3 "pgxn/rfcs#3 Meta Spec v2" ## [v0.1.0] ā€” 2024-08-08 diff --git a/Cargo.lock b/Cargo.lock index a8cc769..846c26f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,7 +238,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pgxn_meta" -version = "0.1.0" +version = "0.2.0" dependencies = [ "assert-json-diff", "boon", diff --git a/Cargo.toml b/Cargo.toml index 4ee838c..cfcd0d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgxn_meta" -version = "0.1.0" +version = "0.2.0" description = "The PGXN distribution metadata specification" repository = "https://github.com/pgxn/pgxn-meta-spec" documentation = "https://docs.rs/pgxn_meta/" diff --git a/Makefile b/Makefile index 240d71c..cee80c4 100644 --- a/Makefile +++ b/Makefile @@ -21,5 +21,7 @@ docs: target/doc/pgxn_meta/index.html target/doc/pgxn_meta/index.html: $(shell find . -name \*.rs) cargo doc -target/release-notes.md: CHANGELOG.md .ci/mknotes Cargo.toml - @./.ci/mknotes -f $< -r https://github.com/$(or $(GITHUB_REPOSITORY),pgxn/meta) -o $@ +VERSION = $(shell perl -nE '/^version\s*=\s*"([^"]+)/ && do { say $$1; exit }' Cargo.toml) +.PHONY: release-notes # Show release notes for current version (must have `mknotes` in PATH). +release-notes: CHANGELOG.md + mknotes -v v$(VERSION) -f $< -r https://github.com/$(or $(GITHUB_REPOSITORY),pgxn/meta) diff --git a/src/lib.rs b/src/lib.rs index 750282c..d90ba74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,48 +3,17 @@ /*! PGXN Metadata validation. -This crate uses JSON Schema to validate PGXN Meta Spec `META.json` files. -It supports both the [v1] and [v2] specs. - -# Example - -``` rust -use std::{path::PathBuf, error::Error}; -use serde_json::json; -use pgxn_meta::*; - -let meta = json!({ - "name": "pair", - "abstract": "A key/value pair data type", - "version": "0.1.8", - "maintainers": [{ "name": "theory", "email": "theory@pgxn.org" }], - "license": "PostgreSQL", - "contents": { - "extensions": { - "pair": { - "sql": "sql/pair.sql", - "control": "pair.control" - } - } - }, - "meta-spec": { "version": "2.0.0" } -}); - -let mut validator = Validator::new(); -assert!(validator.validate(&meta).is_ok()); -# Ok::<(), Box>(()) -``` +This crate uses JSON Schema to validate and inspect the PGXN `META.json` +files. It supports both the [v1] and [v2] specs. [v1]: https://rfcs.pgxn.org/0001-meta-spec-v1.html -[v2]: https://rfcs.pgxn.org/0003-meta-spec-v2.html +[v2]: https://github.com/pgxn/rfcs/pull/3 */ -mod util; -mod valid; -pub use valid::{ValidationError, Validator}; -mod meta; -// pub use meta::*; +pub mod meta; +mod util; // private utilities +pub mod valid; #[cfg(test)] mod tests; diff --git a/src/main.rs b/src/main.rs index 737610b..902ad07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::{ process::ExitCode, }; -use pgxn_meta::Validator; +use pgxn_meta::valid::Validator; use serde_json::Value; // Minimal main function; logical is all in run. diff --git a/src/meta/mod.rs b/src/meta/mod.rs index 6523148..2788a57 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -1,3 +1,13 @@ +/*! +PGXN Metadata management. + +This module provides interfaces to load, validate, and manipulate PGXN Meta +Spec `META.json` files. It supports both the [v1] and [v2] specs. + + [v1]: https://rfcs.pgxn.org/0001-meta-spec-v1.html + [v2]: https://github.com/pgxn/rfcs/pull/3 + +*/ use std::{collections::HashMap, error::Error, fs::File, path::PathBuf}; use crate::util; @@ -247,7 +257,9 @@ pub struct Artifact { sha512: Option, } -/// Represents a complete PGXN Meta definition. +/** +Represents a complete PGXN Meta definition. +*/ #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Meta { name: String, @@ -300,12 +312,13 @@ impl TryFrom for Meta { impl TryFrom<&[&Value]> for Meta { type Error = Box; - // Merge multiple spec values into a single Meta object. The first value - // in `meta` should be the primary metadata, generally included in a - // distribution. Subsequent values will be merged into that first value - // via the [RFC 7396] merge pattern. - // - // [RFC 7396]: https://www.rfc-editor.org/rfc/rfc7396.html + /// Merge multiple spec values into a single Meta object. + /// + /// The first value in `meta` should be the primary metadata, generally + /// included in a distribution. Subsequent values will be merged into that + /// first value via the [RFC 7396] merge pattern. + /// + /// [RFC 7396]: https:///www.rfc-editor.org/rfc/rfc7396.html fn try_from(meta: &[&Value]) -> Result { if meta.is_empty() { return Err(Box::from("meta contains no values")); diff --git a/src/valid/mod.rs b/src/valid/mod.rs index dc3a259..3a4fa08 100644 --- a/src/valid/mod.rs +++ b/src/valid/mod.rs @@ -1,4 +1,42 @@ -//! The valid module provides pgxn_meta validation. +/*! +PGXN Metadata validation. + +This module uses JSON Schema to validate PGXN Meta Spec `META.json` files. +It supports both the [v1] and [v2] specs. + +# Example + +``` rust +# use std::error::Error; +use serde_json::json; +use pgxn_meta::valid::*; + +let meta = json!({ + "name": "pair", + "abstract": "A key/value pair data type", + "version": "0.1.8", + "maintainers": [{ "name": "theory", "email": "theory@pgxn.org" }], + "license": "PostgreSQL", + "contents": { + "extensions": { + "pair": { + "sql": "sql/pair.sql", + "control": "pair.control" + } + } + }, + "meta-spec": { "version": "2.0.0" } +}); + +let mut validator = Validator::new(); +assert!(validator.validate(&meta).is_ok()); +# Ok::<(), Box>(()) +``` + + [v1]: https://rfcs.pgxn.org/0001-meta-spec-v1.html + [v2]: https://github.com/pgxn/rfcs/pull/3 + +*/ use std::{error::Error, fmt}; use boon::{Compiler, Schemas};