Skip to content

Commit

Permalink
Add support for in-package frontmatter bindings:
Browse files Browse the repository at this point in the history
- Enables re-use of the frontmatter inside the package code
- Add tests for frontmatter extraction and parsing
- Add test@v4.0.0 containing this frontmatter type
  • Loading branch information
kMutagene committed Apr 25, 2024
1 parent c3b2c6e commit 0049cf9
Show file tree
Hide file tree
Showing 17 changed files with 664 additions and 220 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Read more at [avpr.nfdi4plants.org/about](https://avpr.nfdi4plants.org/about)
- [Package publication tutorial](#package-publication-tutorial)
- [Versioning packages](#versioning-packages)
- [Package metadata](#package-metadata)
- [YAML frontmatter](#yaml-frontmatter)
- [Frontmatter bindings](#frontmatter-bindings)
- [Mandatory fields](#mandatory-fields)
- [Optional fields](#optional-fields)
- [Objects](#objects)
Expand Down Expand Up @@ -140,6 +142,8 @@ Packages SHOULD be versioned according to the [semantic versioning](https://semv

# Package metadata

## YAML frontmatter

Package metadata is extracted from **yml frontmatter** at the start of the `.fsx` file, indicated by a multiline comment (`(* ... *)`)containing the frontmatter fenced by `---` at its start and end:

```fsharp
Expand All @@ -150,6 +154,35 @@ Package metadata is extracted from **yml frontmatter** at the start of the `.fsx
*)
```

## Frontmatter bindings

You can additionally bind YAML frontmatter as a string inside your package. **This is recommended** because you can now re-use the metadata in your package code.

This binding must be placed at the start of the file to the name `PACKAGE_METADATA` with a `[<Literal>]` attribute _exactly_ like this:

```fsharp
let [<Literal>] PACKAGE_METADATA = """(*
---
<yaml frontmatter here>
---
*)"""
```

further down in your package code, you can now extract and use this metadata. This for example prevents you from having to repeat the package name in your package code.

```fsharp
#r "nuget: ARCExpect"
#r "nuget: AVPRIndex"
let metadata = ValidationPackageMetadata.extractFromString PACKAGE_METADATA
let validationCases = ...
cases
|> Execute.ValidationPipeline(
metadata = metadata // use metadata to determine output paths and names instead of doing it manually
)
```

## Mandatory fields

| Field | Type | Description |
Expand Down
51 changes: 51 additions & 0 deletions StagingArea/test/test@4.0.0.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
let [<Literal>]PACKAGE_METADATA = """(*
---
Name: test
MajorVersion: 4
MinorVersion: 0
PatchVersion: 0
Publish: true
Summary: this package is here for testing purposes only.
Description: this package is here for testing purposes only.
Authors:
- FullName: John Doe
Email: j@d.com
Affiliation: University of Nowhere
AffiliationLink: https://nowhere.edu
- FullName: Jane Doe
Email: jj@d.com
Affiliation: University of Somewhere
AffiliationLink: https://somewhere.edu
Tags:
- Name: validation
- Name: my-package
- Name: thing
ReleaseNotes: "use in-package metadata"
---
*)"""

#r "nuget: ARCExpect, 1.0.1"
#r "nuget: AVPRIndex"

open AVPRIndex.Domain
open AVPRIndex.Frontmatter

let metadata = ValidationPackageMetadata.extractFromString(PACKAGE_METADATA)

// this file is intended for testing purposes only.
printfn "If you can read this in your console, you successfully executed test package v4.0.0!"

#r "nuget: ARCExpect, 1.0.1"

open ARCExpect
open Expecto

let validationCases = testList "test" [
test "yes" {Expect.equal 1 1 "yes"}
]

validationCases
|> Execute.ValidationPipeline(
basePath = System.Environment.CurrentDirectory,
packageName = "test"
)
1 change: 1 addition & 0 deletions src/AVPRIndex/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ open System.Text
open System.Text.Json
open System.Security.Cryptography

[<AutoOpen>]
module Domain =

let jsonSerializerOptions = JsonSerializerOptions(WriteIndented = true)
Expand Down
35 changes: 26 additions & 9 deletions src/AVPRIndex/Frontmatter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,37 @@ open System.IO
open System.Security.Cryptography
open YamlDotNet.Serialization

[<AutoOpen>]
module Frontmatter =

let [<Literal>] frontMatterStart = "(*\n---"
let [<Literal>] frontMatterEnd = "---\n*)"
/// the frontmatter start string if the package uses yaml frontmatter as comment
let [<Literal>] frontMatterCommentStart = "(*\n---"
/// the frontmatter end string if the package uses yaml frontmatter as comment
let [<Literal>] frontMatterCommentEnd = "---\n*)"

let containsFrontmatter (str: string) =
str.StartsWith(frontMatterStart, StringComparison.Ordinal) && str.Contains(frontMatterEnd)
/// the frontmatter start string if the package uses yaml frontmatter as a string binding to be re-used in the package code
let [<Literal>] frontmatterBindingStart = "let [<Literal>]PACKAGE_METADATA = \"\"\"(*\n---"
/// the frontmatter end string if the package uses yaml frontmatter as a string binding to be re-used in the package code
let [<Literal>] frontmatterBindingEnd = "---\n*)\"\"\""


let containsCommentFrontmatter (str: string) =
str.StartsWith(frontMatterCommentStart, StringComparison.Ordinal) && str.Contains(frontMatterCommentEnd)

let containsBindingFrontmatter (str: string) =
str.StartsWith(frontmatterBindingStart, StringComparison.Ordinal) && str.Contains(frontmatterBindingEnd)

let tryExtractFromString (str: string) =
let norm = str.ReplaceLineEndings("\n")
if containsFrontmatter norm then
if containsCommentFrontmatter norm then
norm.Substring(
frontMatterCommentStart.Length,
(norm.IndexOf(frontMatterCommentEnd, StringComparison.Ordinal) - frontMatterCommentStart.Length))
|> Some
elif containsBindingFrontmatter norm then
norm.Substring(
frontMatterStart.Length,
(norm.IndexOf(frontMatterEnd, StringComparison.Ordinal) - frontMatterEnd.Length))
frontmatterBindingStart.Length,
(norm.IndexOf(frontmatterBindingEnd, StringComparison.Ordinal) - frontmatterBindingStart.Length))
|> Some
else
None
Expand All @@ -30,7 +47,7 @@ module Frontmatter =
| Some frontmatter -> frontmatter
| None -> failwith $"input has no correctly formatted frontmatter."

let yamlDeserializer =
let yamlDeserializer() =
DeserializerBuilder()
.WithNamingConvention(NamingConventions.PascalCaseNamingConvention.Instance)
.Build()
Expand All @@ -42,7 +59,7 @@ module Frontmatter =
match frontmatter with
| Some frontmatter ->
let result =
yamlDeserializer.Deserialize<ValidationPackageMetadata>(frontmatter)
yamlDeserializer().Deserialize<ValidationPackageMetadata>(frontmatter)
result
| None ->
failwith $"string has no correctly formatted frontmatter."
Expand Down
29 changes: 0 additions & 29 deletions tests/Common/Common.fsproj

This file was deleted.

1 change: 0 additions & 1 deletion tests/Common/Program.fs

This file was deleted.

8 changes: 0 additions & 8 deletions tests/Common/Utils.fs

This file was deleted.

160 changes: 113 additions & 47 deletions tests/IndexTests/FrontmatterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,130 @@ open Xunit
open AVPRIndex
open ReferenceObjects

module InMemory =

[<Fact>]
let ``valid frontmatter capture guides lead to results`` () =
Assert.All(
[Frontmatter.validMandatoryFrontmatter; Frontmatter.validFullFrontmatter; Frontmatter.invalidMissingMandatoryFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsSome)
module Comment =

module InMemory =

[<Fact>]
let ``valid frontmatter capture guides lead to results`` () =
Assert.All(
[Frontmatter.Comment.validMandatoryFrontmatter; Frontmatter.Comment.validFullFrontmatter; Frontmatter.Comment.invalidMissingMandatoryFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsSome)
)
)

[<Fact>]
let ``valid frontmatter capture guides lead to correctly extracted substrings`` () =
Assert.All(
[
Frontmatter.Comment.validMandatoryFrontmatter, Frontmatter.Comment.validMandatoryFrontmatterExtracted
Frontmatter.Comment.validFullFrontmatter, Frontmatter.Comment.validFullFrontmatterExtracted
Frontmatter.Comment.invalidMissingMandatoryFrontmatter, Frontmatter.Comment.invalidMissingMandatoryFrontmatterExtracted
],
(fun (fm, expected) ->
let actual = Frontmatter.extractFromString fm
Assert.Equal(expected, actual)
)
)
)

[<Fact>]
let ``valid frontmatter capture guides lead to correctly extracted substrings`` () =
Assert.All(
[
Frontmatter.validMandatoryFrontmatter, Frontmatter.validMandatoryFrontmatterExtracted
Frontmatter.validFullFrontmatter, Frontmatter.validFullFrontmatterExtracted
Frontmatter.invalidMissingMandatoryFrontmatter, Frontmatter.invalidMissingMandatoryFrontmatterExtracted
],
(fun (fm, expected) ->
let actual = Frontmatter.extractFromString fm
Assert.Equal(expected, actual)

[<Fact>]
let ``invalid frontmatter capture substrings are leading to None`` () =
Assert.All(
[Frontmatter.Comment.invalidEndFrontmatter; Frontmatter.Comment.invalidStartFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsNone)
)
)
)

[<Fact>]
let ``invalid frontmatter capture substrings are leading to None`` () =
Assert.All(
[Frontmatter.invalidEndFrontmatter; Frontmatter.invalidStartFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsNone)

module IO =

open System.IO

[<Fact>]
let ``valid frontmatter substring is extracted from valid mandatory field test file`` () =

let actual = File.ReadAllText("fixtures/Frontmatter/Comment/valid@1.0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.Comment.validMandatoryFrontmatterExtracted, actual.Value)

[<Fact>]
let ``valid frontmatter substring is correctly from all fields test file`` () =

let actual = File.ReadAllText("fixtures/Frontmatter/Comment//valid@2.0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.Comment.validFullFrontmatterExtracted, actual.Value)

[<Fact>]
let ``frontmatter substring is extracted although metadata is missing fields`` () =

let actual = File.ReadAllText("fixtures/Frontmatter/Comment//invalid@0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.Comment.invalidMissingMandatoryFrontmatterExtracted, actual.Value)

module Binding =

module InMemory =

[<Fact>]
let ``valid frontmatter capture guides lead to results`` () =
Assert.All(
[Frontmatter.Binding.validMandatoryFrontmatter; Frontmatter.Binding.validFullFrontmatter; Frontmatter.Binding.invalidMissingMandatoryFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsSome)
)
)

[<Fact>]
let ``valid frontmatter capture guides lead to correctly extracted substrings`` () =
Assert.All(
[
Frontmatter.Binding.validMandatoryFrontmatter, Frontmatter.Binding.validMandatoryFrontmatterExtracted
Frontmatter.Binding.validFullFrontmatter, Frontmatter.Binding.validFullFrontmatterExtracted
Frontmatter.Binding.invalidMissingMandatoryFrontmatter, Frontmatter.Binding.invalidMissingMandatoryFrontmatterExtracted
],
(fun (fm, expected) ->
let actual = Frontmatter.extractFromString fm
Assert.Equal(expected, actual)
)
)

[<Fact>]
let ``invalid frontmatter capture substrings are leading to None`` () =
Assert.All(
[Frontmatter.Binding.invalidEndFrontmatter; Frontmatter.Binding.invalidStartFrontmatter],
(fun fm ->
Assert.True ((Frontmatter.tryExtractFromString fm).IsNone)
)
)
)

module IO =
module IO =

open System.IO
open System.IO

[<Fact>]
let ``valid frontmatter substring is extracted from valid mandatory field test file`` () =
[<Fact>]
let ``valid frontmatter substring is extracted from valid mandatory field test file`` () =

let actual = File.ReadAllText("fixtures/valid@1.0.0.fsx") |> Frontmatter.tryExtractFromString
let actual = File.ReadAllText("fixtures/Frontmatter/Binding/valid@1.0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.validMandatoryFrontmatterExtracted, actual.Value)
Assert.True actual.IsSome
Assert.Equal (Frontmatter.Binding.validMandatoryFrontmatterExtracted, actual.Value)

[<Fact>]
let ``valid frontmatter substring is correctly from all fields test file`` () =
[<Fact>]
let ``valid frontmatter substring is correctly from all fields test file`` () =

let actual = File.ReadAllText("fixtures/valid@2.0.0.fsx") |> Frontmatter.tryExtractFromString
let actual = File.ReadAllText("fixtures/Frontmatter/Binding/valid@2.0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.validFullFrontmatterExtracted, actual.Value)
Assert.True actual.IsSome
Assert.Equal (Frontmatter.Binding.validFullFrontmatterExtracted, actual.Value)

[<Fact>]
let ``frontmatter substring is extracted although metadata is missing fields`` () =
[<Fact>]
let ``frontmatter substring is extracted although metadata is missing fields`` () =

let actual = File.ReadAllText("fixtures/invalid@0.0.fsx") |> Frontmatter.tryExtractFromString
let actual = File.ReadAllText("fixtures/Frontmatter/Binding/invalid@0.0.fsx") |> Frontmatter.tryExtractFromString

Assert.True actual.IsSome
Assert.Equal (Frontmatter.invalidMissingMandatoryFrontmatterExtracted, actual.Value)
Assert.True actual.IsSome
Assert.Equal (Frontmatter.Binding.invalidMissingMandatoryFrontmatterExtracted, actual.Value)
Loading

0 comments on commit 0049cf9

Please sign in to comment.