Compare commits

..

22 Commits

Author SHA1 Message Date
Patrick Stevens
67e051b6b3 Add --no- prefix for bools (#455)
Implements support for --no- prefix negation on boolean and flag DU fields
when marked with [<ArgumentNegateWithPrefix>]. This allows both --flag and
--no-flag forms to be accepted, with --no- variants negating the value.

Changes:
- Extend ArgParserGenerator to generate --no- prefix handling
- Add conflict detection for overlapping --no- prefixed arguments
- Update help text to display both forms (e.g., --verbose / --no-verbose)
- Add test examples in ArgParserNegationTests.fs demonstrating:
  - Boolean field negation
  - Flag DU negation
  - Multiple ArgumentLongForm with negation
  - Combined features (defaults, help text)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 07:53:15 +00:00
dependabot[bot]
6dee454229 Bump ApiSurface from 5.0.2 to 5.0.3 (#452)
* Bump ApiSurface from 5.0.2 to 5.0.3

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: ApiSurface
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump fsharp-analyzers from 0.33.1 to 0.34.1

---
updated-dependencies:
- dependency-name: fsharp-analyzers
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Microsoft.NET.Test.Sdk from 18.0.0 to 18.0.1

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

* Analyzers too

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-11-17 22:29:40 +00:00
patrick-conscriptus[bot]
e1767f5ed0 Automated commit (#451)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-16 01:43:42 +00:00
dependabot[bot]
f95437aa48 Bump FsCheck from 3.3.1 to 3.3.2 (#449)
* Bump FsCheck from 3.3.1 to 3.3.2

---
updated-dependencies:
- dependency-name: FsCheck
  dependency-version: 3.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Nerdbank.GitVersioning from 3.8.118 to 3.9.50

---
updated-dependencies:
- dependency-name: Nerdbank.GitVersioning
  dependency-version: 3.9.50
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-11-10 17:50:04 +00:00
patrick-conscriptus[bot]
e9edf9dabc Automated commit (#448)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-09 01:33:34 +00:00
patrick-conscriptus[bot]
d873850acf Automated commit (#447)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-11-02 01:41:54 +00:00
dependabot[bot]
bd4905a236 Bump actions/upload-artifact from 4 to 5 (#446)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Stevens <3138005+Smaug123@users.noreply.github.com>
2025-10-30 07:43:57 +00:00
dependabot[bot]
62daa84b26 Bump actions/download-artifact from 5 to 6 (#445) 2025-10-27 11:11:53 +00:00
patrick-conscriptus[bot]
4d5830d147 Automated commit (#444)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-26 01:32:39 +00:00
patrick-conscriptus[bot]
6c2280c300 Automated commit (#443)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-24 19:57:59 +00:00
dependabot[bot]
ecbe425f66 Bump WoofWare.Expect from 0.8.3 to 0.8.4 (#442)
* Bump WoofWare.Expect from 0.8.3 to 0.8.4

---
updated-dependencies:
- dependency-name: WoofWare.Expect
  dependency-version: 0.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-20 13:15:33 +00:00
dependabot[bot]
30eb28d00a Bump WoofWare.Whippet.Fantomas from 0.6.3 to 0.6.4 (#440) 2025-10-20 11:13:03 +00:00
patrick-conscriptus[bot]
62e1cf2c46 Upgrade Nix flake and deps (#439)
* Automated commit

* Fix link

---------

Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-19 04:54:20 +00:00
dependabot[bot]
fa81119cec Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0 (#434)
* Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump NUnit3TestAdapter from 5.0.0 to 5.2.0

---
updated-dependencies:
- dependency-name: NUnit3TestAdapter
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: NUnit3TestAdapter
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump WoofWare.Expect from 0.8.2 to 0.8.3

---
updated-dependencies:
- dependency-name: WoofWare.Expect
  dependency-version: 0.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump ApiSurface from 5.0.1 to 5.0.2

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: ApiSurface
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump TypeEquality from 0.3.0 to 0.4.2

---
updated-dependencies:
- dependency-name: TypeEquality
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-10-13 17:39:07 +00:00
patrick-conscriptus[bot]
7907cefaee Automated commit (#433)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-12 01:29:47 +00:00
patrick-conscriptus[bot]
e83ec1f152 Automated commit (#432)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-10-05 01:31:29 +00:00
Patrick Stevens
9d8cef8fdc Switch to trusted publishing (#431) 2025-10-03 09:37:32 +00:00
Patrick Stevens
1721ad1ac0 Unconditional function for empty generating mock (#430) 2025-09-30 21:52:59 +00:00
dependabot[bot]
857bde0ba9 Bump Nerdbank.GitVersioning from 3.8.38-alpha to 3.8.118 (#428)
* Bump Nerdbank.GitVersioning from 3.8.38-alpha to 3.8.118

---
updated-dependencies:
- dependency-name: Nerdbank.GitVersioning
  dependency-version: 3.8.118
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-09-29 23:52:31 +01:00
patrick-conscriptus[bot]
d10f608941 Automated commit (#427)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-09-28 01:31:56 +00:00
patrick-conscriptus[bot]
46effedfc4 Automated commit (#426)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-09-21 01:31:20 +00:00
Patrick Stevens
1b85182b9d GenerateCapturingMock that captures calls made to it (#425) 2025-09-18 15:42:10 +01:00
28 changed files with 2957 additions and 465 deletions

View File

@@ -9,7 +9,7 @@
]
},
"fsharp-analyzers": {
"version": "0.32.1",
"version": "0.34.1",
"commands": [
"fsharp-analyzers"
]

View File

@@ -167,12 +167,12 @@ jobs:
- name: Pack
run: nix develop --command dotnet pack --configuration Release
- name: Upload NuGet artifact (plugin)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: nuget-package-plugin
path: WoofWare.Myriad.Plugins/bin/Release/WoofWare.Myriad.Plugins.*.nupkg
- name: Upload NuGet artifact (attributes)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: nuget-package-attribute
path: WoofWare.Myriad.Plugins.Attributes/bin/Release/WoofWare.Myriad.Plugins.Attributes.*.nupkg
@@ -182,7 +182,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download NuGet artifact (plugin)
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-plugin
path: packed-plugin
@@ -190,7 +190,7 @@ jobs:
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
run: if [[ $(find packed-plugin -maxdepth 1 -name 'WoofWare.Myriad.Plugins.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
- name: Download NuGet artifact (attributes)
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-attribute
path: packed-attribute
@@ -209,7 +209,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: ${{ matrix.artifact }}
- name: Compute package path
@@ -249,7 +249,7 @@ jobs:
contents: read
steps:
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-attribute
path: packed
@@ -268,7 +268,7 @@ jobs:
contents: read
steps:
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-plugin
path: packed
@@ -294,19 +294,24 @@ jobs:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-attribute
path: packed
- name: Identify `dotnet`
id: dotnet-identify
run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT'
- name: Obtain NuGet key
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet
id: publish-success
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
with:
package-name: WoofWare.Myriad.Plugins.Attributes
nuget-key: ${{ secrets.NUGET_API_KEY }}
nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
nupkg-dir: packed/
dotnet: ${{ steps.dotnet-identify.outputs.dotnet }}
@@ -327,19 +332,24 @@ jobs:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: nuget-package-plugin
path: packed
- name: Identify `dotnet`
id: dotnet-identify
run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT'
- name: Obtain NuGet key
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet
id: publish-success
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
with:
package-name: WoofWare.Myriad.Plugins
nuget-key: ${{ secrets.NUGET_API_KEY }}
nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
nupkg-dir: packed/
dotnet: ${{ steps.dotnet-identify.outputs.dotnet }}
@@ -358,7 +368,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Download NuGet artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: ${{ matrix.artifact }}
- name: Compute package path

View File

@@ -1,5 +1,16 @@
Notable changes are recorded here.
# WoofWare.Myriad.Plugins 9.1.1, WoofWare.Myriad.Plugins.Attributes 3.8.1
Adds the `[<ArgumentNegateWithPrefix>]` attribute, which can be placed on a boolean or flag-valued field when using the `ArgParser` generator.
This causes the boolean to be specifiable with the `--no-` prefix to negate its value.
(For example, `Foo : bool` is normally specified as `--foo`; this new attribute lets the user additionally give `--no-foo` to get the same semantics as `--foo=false`.)
# WoofWare.Myriad.Plugins 9.0.1
Converts the `static member Empty` field on each generated mock (from `GeneratedMock`) into a function, so as to permit the `GeneratedCapturingMock` to have the same signature.
(`GeneratedCapturingMock` contains mutable state, so must be created afresh each time.)
# WoofWare.Myriad.Plugins 8.1.1
Adds `GenerateCapturingMock`, which is `GenerateMock` but additionally records the calls made to each function.

View File

@@ -44,7 +44,7 @@ git config blame.ignoreRevsFile .git-blame-ignore-revs
## Dependencies
I try to keep this repository's dependencies as few as possible, because (for example) any consumer of the source generator will also consume this project via the attributes.
When adding dependencies, you will need to `nix run .#fetchDeps` to obtain a new copy of [the dependency lockfile](./nix/deps.nix).
When adding dependencies, you will need to `nix run .#fetchDeps` to obtain a new copy of [the dependency lockfile](./nix/deps.json).
## Branch strategy

View File

@@ -0,0 +1,124 @@
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
// This file contains test cases for conflict detection in the ArgParser generator.
// These are expected to FAIL at build time with appropriate error messages.
// Uncomment each section one at a time to test the specific conflict detection.
// ============================================================================
// Test 1: Field named NoFooBar conflicts with FooBar's --no- variant
// ============================================================================
// Expected error: Argument name conflict: '--no-foo-bar' collides with the --no- variant
// of field 'FooBar' (which has [<ArgumentNegateWithPrefix>])
(*
[<ArgParser>]
type ConflictingFieldNames =
{
[<ArgumentNegateWithPrefix>]
FooBar : bool
NoFooBar : bool
}
*)
// ============================================================================
// Test 2: ArgumentLongForm "no-foo" conflicts with Foo's --no- variant
// ============================================================================
// Expected error: Argument name conflict: '--no-foo' collides with the --no- variant
// of field 'Foo' (which has [<ArgumentNegateWithPrefix>])
(*
[<ArgParser>]
type ConflictingLongForm =
{
[<ArgumentNegateWithPrefix>]
Foo : bool
[<ArgumentLongForm "no-foo">]
Bar : bool
}
*)
// ============================================================================
// Test 3: Multiple ArgumentLongForm, one conflicts
// ============================================================================
// Expected error: Argument name conflict: '--no-verbose' collides with...
(*
[<ArgParser>]
type ConflictingMultipleLongForms =
{
[<ArgumentLongForm "verbose">]
[<ArgumentLongForm "v">]
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
[<ArgumentLongForm "no-verbose">]
Quiet : bool
}
*)
// ============================================================================
// Test 4: ArgumentNegateWithPrefix on non-boolean field
// ============================================================================
// Expected error: [<ArgumentNegateWithPrefix>] can only be applied to boolean
// or flag DU fields, but was applied to field NotABool of type string
(*
[<ArgParser>]
type InvalidAttributeOnNonBool =
{
[<ArgumentNegateWithPrefix>]
NotABool : string
}
*)
// ============================================================================
// Test 5: ArgumentNegateWithPrefix on non-flag int field
// ============================================================================
// Expected error: [<ArgumentNegateWithPrefix>] can only be applied to boolean
// or flag DU fields
(*
[<ArgParser>]
type InvalidAttributeOnInt =
{
[<ArgumentNegateWithPrefix>]
NotAFlag : int
}
*)
// ============================================================================
// Test 6: Complex conflict with custom names
// ============================================================================
// This tests a more complex scenario where a custom ArgumentLongForm creates
// a conflict with a different field's negated form
(*
[<ArgParser>]
type ComplexConflict =
{
[<ArgumentLongForm "enable">]
[<ArgumentNegateWithPrefix>]
FeatureA : bool
[<ArgumentLongForm "no-enable">]
DisableAll : bool
}
*)
// ============================================================================
// Test 7: Valid usage - no conflicts (this SHOULD compile)
// ============================================================================
[<ArgParser>]
type NoConflict =
{
[<ArgumentNegateWithPrefix>]
EnableFeature : bool
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
NormalField : string
}

View File

@@ -0,0 +1,48 @@
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
// Test types for ArgumentNegateWithPrefix functionality
type TestDryRunMode =
| [<ArgumentFlag false>] Wet
| [<ArgumentFlag true>] Dry
[<ArgParser true>]
type BoolNegation =
{
[<ArgumentNegateWithPrefix>]
EnableFeature : bool
}
[<ArgParser true>]
type FlagNegation =
{
[<ArgumentNegateWithPrefix>]
DryRun : TestDryRunMode
}
[<ArgParser true>]
type MultipleFormsNegation =
{
[<ArgumentLongForm "verbose">]
[<ArgumentLongForm "v">]
[<ArgumentNegateWithPrefix>]
VerboseMode : bool
}
[<ArgParser true>]
type CombinedFeatures =
{
[<ArgumentNegateWithPrefix>]
[<ArgumentDefaultFunction>]
Verbose : Choice<bool, bool>
[<ArgumentNegateWithPrefix>]
[<ArgumentHelpText "Enable debug mode">]
Debug : bool
NormalBool : bool
}
static member DefaultVerbose () = false

View File

@@ -89,6 +89,17 @@
<Compile Include="GeneratedArgs.fs">
<MyriadFile>Args.fs</MyriadFile>
</Compile>
<Compile Include="ArgParserNegationTests.fs" />
<Compile Include="GeneratedArgParserNegationTests.fs">
<MyriadFile>ArgParserNegationTests.fs</MyriadFile>
</Compile>
<!-- Not compiled, because by design they *don't* compile. That makes them very hard to test in an automated way! -->
<None Include="ArgParserConflictTests.fs" />
<!-- To run the conflict tests:
<Compile Include="GeneratedArgParserConflictTests.fs">
<MyriadFile>ArgParserConflictTests.fs</MyriadFile>
</Compile>
-->
<None Include="swagger-gitea.json" />
<Compile Include="GeneratedSwaggerGitea.fs">
<MyriadFile>swagger-gitea.json</MyriadFile>

File diff suppressed because it is too large Load Diff

View File

@@ -9,180 +9,285 @@ namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal PublicTypeMockCalls =
/// All the calls made to a PublicTypeMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
Mem3 : ResizeArray<int * System.Threading.CancellationToken option>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
}
/// Mock record type for an interface
type internal PublicTypeMock =
{
Calls : PublicTypeMockCalls.Calls
Mem1 : string * int -> string list
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
Mem3 : int * option<System.Threading.CancellationToken> -> string
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem3_Calls : ResizeArray<int * System.Threading.CancellationToken>
Mem3 : int * System.Threading.CancellationToken option -> string
}
/// An implementation where every non-unit method throws.
static member Empty : PublicTypeMock =
static member Empty () : PublicTypeMock =
{
Calls = PublicTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
}
interface IPublicType with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) =
lock this.Calls.Mem3 (fun _ -> this.Calls.Mem3.Add (arg_0_0, arg_0_1))
this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module public PublicTypeInternalFalseMockCalls =
/// All the calls made to a PublicTypeInternalFalseMock mock
type public Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
Mem3 : ResizeArray<int * System.Threading.CancellationToken option>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
}
/// Mock record type for an interface
type public PublicTypeInternalFalseMock =
{
Calls : PublicTypeInternalFalseMockCalls.Calls
Mem1 : string * int -> string list
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
Mem3 : int * option<System.Threading.CancellationToken> -> string
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem3_Calls : ResizeArray<int * System.Threading.CancellationToken>
Mem3 : int * System.Threading.CancellationToken option -> string
}
/// An implementation where every non-unit method throws.
static member Empty : PublicTypeInternalFalseMock =
static member Empty () : PublicTypeInternalFalseMock =
{
Calls = PublicTypeInternalFalseMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
}
interface IPublicTypeInternalFalse with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) =
lock this.Calls.Mem3 (fun _ -> this.Calls.Mem3.Add (arg_0_0, arg_0_1))
this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal InternalTypeMockCalls =
/// All the calls made to a InternalTypeMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type internal InternalTypeMock =
{
Calls : InternalTypeMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : InternalTypeMock =
static member Empty () : InternalTypeMock =
{
Calls = InternalTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface InternalType with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal PrivateTypeMockCalls =
/// All the calls made to a PrivateTypeMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type private PrivateTypeMock =
{
Calls : PrivateTypeMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : PrivateTypeMock =
static member Empty () : PrivateTypeMock =
{
Calls = PrivateTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface PrivateType with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal PrivateTypeInternalFalseMockCalls =
/// All the calls made to a PrivateTypeInternalFalseMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type private PrivateTypeInternalFalseMock =
{
Calls : PrivateTypeInternalFalseMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : PrivateTypeInternalFalseMock =
static member Empty () : PrivateTypeInternalFalseMock =
{
Calls = PrivateTypeInternalFalseMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface PrivateTypeInternalFalse with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal VeryPublicTypeMockCalls =
/// All the calls made to a VeryPublicTypeMock mock
type internal Calls<'a, 'b> =
{
Mem1 : ResizeArray<'a>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls<'a, 'b> =
{
Mem1 = ResizeArray ()
}
/// Mock record type for an interface
type internal VeryPublicTypeMock<'a, 'b> =
{
Calls : VeryPublicTypeMockCalls.Calls<'a, 'b>
Mem1 : 'a -> 'b
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<'a>
}
/// An implementation where every non-unit method throws.
static member Empty () : VeryPublicTypeMock<'a, 'b> =
{
Calls = VeryPublicTypeMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem1_Calls = ResizeArray ()
}
interface VeryPublicType<'a, 'b> with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal CurriedMockCalls =
/// A single call to the Mem1 method
type internal Mem1Call<'a> =
@@ -226,84 +331,178 @@ module internal CurriedMockCalls =
Arg1 : 'a * int
}
/// All the calls made to a CurriedMock mock
type internal Calls<'a> =
{
Mem1 : ResizeArray<Mem1Call<'a>>
Mem2 : ResizeArray<Mem2Call<'a>>
Mem3 : ResizeArray<Mem3Call<'a>>
Mem4 : ResizeArray<Mem4Call<'a>>
Mem5 : ResizeArray<Mem5Call<'a>>
Mem6 : ResizeArray<Mem6Call<'a>>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls<'a> =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
Mem4 = ResizeArray ()
Mem5 = ResizeArray ()
Mem6 = ResizeArray ()
}
/// Mock record type for an interface
type internal CurriedMock<'a> =
{
Calls : CurriedMockCalls.Calls<'a>
Mem1 : int -> 'a -> string
Mem1_Calls : ResizeArray<CurriedMockCalls.Mem1Call<'a>>
Mem2 : int * string -> 'a -> string
Mem2_Calls : ResizeArray<CurriedMockCalls.Mem2Call<'a>>
Mem3 : (int * string) -> 'a -> string
Mem3_Calls : ResizeArray<CurriedMockCalls.Mem3Call<'a>>
Mem4 : (int * string) -> ('a * int) -> string
Mem4_Calls : ResizeArray<CurriedMockCalls.Mem4Call<'a>>
Mem5 : int * string -> ('a * int) -> string
Mem5_Calls : ResizeArray<CurriedMockCalls.Mem5Call<'a>>
Mem6 : int * string -> 'a * int -> string
Mem6_Calls : ResizeArray<CurriedMockCalls.Mem6Call<'a>>
}
/// An implementation where every non-unit method throws.
static member Empty () : CurriedMock<'a> =
{
Calls = CurriedMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem4 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem4"))
Mem5 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem5"))
Mem6 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem6"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
Mem4_Calls = ResizeArray ()
Mem5_Calls = ResizeArray ()
Mem6_Calls = ResizeArray ()
}
interface Curried<'a> with
member this.Mem1 arg_0_0 arg_1_0 = this.Mem1 (arg_0_0) (arg_1_0)
member this.Mem2 (arg_0_0, arg_0_1) arg_1_0 = this.Mem2 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem3 arg_0_0 arg_1_0 = this.Mem3 (arg_0_0) (arg_1_0)
member this.Mem1 arg_0_0 arg_1_0 =
lock
this.Calls.Mem1
(fun _ ->
this.Calls.Mem1.Add
{
bar = arg_0_0
Arg1 = arg_1_0
}
)
this.Mem1 (arg_0_0) (arg_1_0)
member this.Mem2 (arg_0_0, arg_0_1) arg_1_0 =
lock
this.Calls.Mem2
(fun _ ->
this.Calls.Mem2.Add
{
Arg0 = arg_0_0, arg_0_1
baz = arg_1_0
}
)
this.Mem2 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem3 arg_0_0 arg_1_0 =
lock
this.Calls.Mem3
(fun _ ->
this.Calls.Mem3.Add
{
quux = arg_0_0
flurb = arg_1_0
}
)
this.Mem3 (arg_0_0) (arg_1_0)
member this.Mem4 ((arg_0_0, arg_0_1)) ((arg_1_0, arg_1_1)) =
lock
this.Calls.Mem4
(fun _ ->
this.Calls.Mem4.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem4 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem5 (arg_0_0, arg_0_1) ((arg_1_0, arg_1_1)) =
lock
this.Calls.Mem5
(fun _ ->
this.Calls.Mem5.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem5 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1) =
lock
this.Calls.Mem6
(fun _ ->
this.Calls.Mem6.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal TypeWithInterfaceMockCalls =
/// All the calls made to a TypeWithInterfaceMock mock
type internal Calls =
{
Mem1 : ResizeArray<string option>
Mem2 : ResizeArray<unit>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type internal TypeWithInterfaceMock =
{
Calls : TypeWithInterfaceMockCalls.Calls
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
Mem1 : string option -> string[] Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string option>
Mem2 : unit -> string[] Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<unit>
}
/// An implementation where every non-unit method throws.
static member Empty : TypeWithInterfaceMock =
static member Empty () : TypeWithInterfaceMock =
{
Calls = TypeWithInterfaceMockCalls.Calls.Empty ()
Dispose = (fun () -> ())
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface TypeWithInterface with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem2 () = this.Mem2 (())
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
member this.Mem2 () =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (()))
this.Mem2 (())
interface System.IDisposable with
member this.Dispose () : unit = this.Dispose ()
@@ -312,36 +511,50 @@ namespace SomeNamespace.CapturingMock
open System
open WoofWare.Myriad.Plugins
[<RequireQualifiedAccess>]
module internal TypeWithPropertiesMockCalls =
/// All the calls made to a TypeWithPropertiesMock mock
type internal Calls =
{
Mem1 : ResizeArray<string option>
Prop1 : ResizeArray<unit>
Prop2 : ResizeArray<unit>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Prop1 = ResizeArray ()
Prop2 = ResizeArray ()
}
/// Mock record type for an interface
type internal TypeWithPropertiesMock =
{
Calls : TypeWithPropertiesMockCalls.Calls
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
Prop1 : unit -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Prop1_Calls : ResizeArray<unit>
Prop2 : unit -> unit Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Prop2_Calls : ResizeArray<unit>
Mem1 : string option -> string[] Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string option>
Prop1 : unit -> int
Prop2 : unit -> unit Async
}
/// An implementation where every non-unit method throws.
static member Empty : TypeWithPropertiesMock =
static member Empty () : TypeWithPropertiesMock =
{
Calls = TypeWithPropertiesMockCalls.Calls.Empty ()
Dispose = (fun () -> ())
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Prop1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Prop1"))
Prop2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Prop2"))
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Prop1_Calls = ResizeArray ()
Prop2_Calls = ResizeArray ()
Mem1_Calls = ResizeArray ()
}
interface TypeWithProperties with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
member this.Prop1 = this.Prop1 ()
member this.Prop2 = this.Prop2 ()

View File

@@ -8,174 +8,279 @@ namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal PublicTypeNoAttrMockCalls =
/// All the calls made to a PublicTypeNoAttrMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
Mem3 : ResizeArray<int * System.Threading.CancellationToken option>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
}
/// Mock record type for an interface
type internal PublicTypeNoAttrMock =
{
Calls : PublicTypeNoAttrMockCalls.Calls
Mem1 : string * int -> string list
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
Mem3 : int * option<System.Threading.CancellationToken> -> string
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem3_Calls : ResizeArray<int * System.Threading.CancellationToken>
Mem3 : int * System.Threading.CancellationToken option -> string
}
/// An implementation where every non-unit method throws.
static member Empty : PublicTypeNoAttrMock =
static member Empty () : PublicTypeNoAttrMock =
{
Calls = PublicTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
}
interface IPublicTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) =
lock this.Calls.Mem3 (fun _ -> this.Calls.Mem3.Add (arg_0_0, arg_0_1))
this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module public PublicTypeInternalFalseNoAttrMockCalls =
/// All the calls made to a PublicTypeInternalFalseNoAttrMock mock
type public Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
Mem3 : ResizeArray<int * System.Threading.CancellationToken option>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
}
/// Mock record type for an interface
type public PublicTypeInternalFalseNoAttrMock =
{
Calls : PublicTypeInternalFalseNoAttrMockCalls.Calls
Mem1 : string * int -> string list
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
Mem3 : int * option<System.Threading.CancellationToken> -> string
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem3_Calls : ResizeArray<int * System.Threading.CancellationToken>
Mem3 : int * System.Threading.CancellationToken option -> string
}
/// An implementation where every non-unit method throws.
static member Empty : PublicTypeInternalFalseNoAttrMock =
static member Empty () : PublicTypeInternalFalseNoAttrMock =
{
Calls = PublicTypeInternalFalseNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
}
interface IPublicTypeInternalFalseNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) =
lock this.Calls.Mem3 (fun _ -> this.Calls.Mem3.Add (arg_0_0, arg_0_1))
this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal InternalTypeNoAttrMockCalls =
/// All the calls made to a InternalTypeNoAttrMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type internal InternalTypeNoAttrMock =
{
Calls : InternalTypeNoAttrMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : InternalTypeNoAttrMock =
static member Empty () : InternalTypeNoAttrMock =
{
Calls = InternalTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface InternalTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal PrivateTypeNoAttrMockCalls =
/// All the calls made to a PrivateTypeNoAttrMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type private PrivateTypeNoAttrMock =
{
Calls : PrivateTypeNoAttrMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : PrivateTypeNoAttrMock =
static member Empty () : PrivateTypeNoAttrMock =
{
Calls = PrivateTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface PrivateTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal PrivateTypeInternalFalseNoAttrMockCalls =
/// All the calls made to a PrivateTypeInternalFalseNoAttrMock mock
type internal Calls =
{
Mem1 : ResizeArray<string * int>
Mem2 : ResizeArray<string>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type private PrivateTypeInternalFalseNoAttrMock =
{
Calls : PrivateTypeInternalFalseNoAttrMockCalls.Calls
Mem1 : string * int -> unit
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string * int>
Mem2 : string -> int
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<string>
}
/// An implementation where every non-unit method throws.
static member Empty : PrivateTypeInternalFalseNoAttrMock =
static member Empty () : PrivateTypeInternalFalseNoAttrMock =
{
Calls = PrivateTypeInternalFalseNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface PrivateTypeInternalFalseNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem1 (arg_0_0, arg_0_1) =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0, arg_0_1))
this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (arg_0_0))
this.Mem2 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal VeryPublicTypeNoAttrMockCalls =
/// All the calls made to a VeryPublicTypeNoAttrMock mock
type internal Calls<'a, 'b> =
{
Mem1 : ResizeArray<'a>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls<'a, 'b> =
{
Mem1 = ResizeArray ()
}
/// Mock record type for an interface
type internal VeryPublicTypeNoAttrMock<'a, 'b> =
{
Calls : VeryPublicTypeNoAttrMockCalls.Calls<'a, 'b>
Mem1 : 'a -> 'b
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<'a>
}
/// An implementation where every non-unit method throws.
static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> =
{
Calls = VeryPublicTypeNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem1_Calls = ResizeArray ()
}
interface VeryPublicTypeNoAttr<'a, 'b> with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal CurriedNoAttrMockCalls =
/// A single call to the Mem1 method
type internal Mem1Call<'a> =
@@ -219,83 +324,177 @@ module internal CurriedNoAttrMockCalls =
Arg1 : 'a * int
}
/// All the calls made to a CurriedNoAttrMock mock
type internal Calls<'a> =
{
Mem1 : ResizeArray<Mem1Call<'a>>
Mem2 : ResizeArray<Mem2Call<'a>>
Mem3 : ResizeArray<Mem3Call<'a>>
Mem4 : ResizeArray<Mem4Call<'a>>
Mem5 : ResizeArray<Mem5Call<'a>>
Mem6 : ResizeArray<Mem6Call<'a>>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls<'a> =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
Mem3 = ResizeArray ()
Mem4 = ResizeArray ()
Mem5 = ResizeArray ()
Mem6 = ResizeArray ()
}
/// Mock record type for an interface
type internal CurriedNoAttrMock<'a> =
{
Calls : CurriedNoAttrMockCalls.Calls<'a>
Mem1 : int -> 'a -> string
Mem1_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem1Call<'a>>
Mem2 : int * string -> 'a -> string
Mem2_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem2Call<'a>>
Mem3 : (int * string) -> 'a -> string
Mem3_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem3Call<'a>>
Mem4 : (int * string) -> ('a * int) -> string
Mem4_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem4Call<'a>>
Mem5 : int * string -> ('a * int) -> string
Mem5_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem5Call<'a>>
Mem6 : int * string -> 'a * int -> string
Mem6_Calls : ResizeArray<CurriedNoAttrMockCalls.Mem6Call<'a>>
}
/// An implementation where every non-unit method throws.
static member Empty () : CurriedNoAttrMock<'a> =
{
Calls = CurriedNoAttrMockCalls.Calls.Empty ()
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem4 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem4"))
Mem5 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem5"))
Mem6 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem6"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Mem3_Calls = ResizeArray ()
Mem4_Calls = ResizeArray ()
Mem5_Calls = ResizeArray ()
Mem6_Calls = ResizeArray ()
}
interface CurriedNoAttr<'a> with
member this.Mem1 arg_0_0 arg_1_0 = this.Mem1 (arg_0_0) (arg_1_0)
member this.Mem2 (arg_0_0, arg_0_1) arg_1_0 = this.Mem2 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem3 ((arg_0_0, arg_0_1)) arg_1_0 = this.Mem3 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem1 arg_0_0 arg_1_0 =
lock
this.Calls.Mem1
(fun _ ->
this.Calls.Mem1.Add
{
Arg0 = arg_0_0
Arg1 = arg_1_0
}
)
this.Mem1 (arg_0_0) (arg_1_0)
member this.Mem2 (arg_0_0, arg_0_1) arg_1_0 =
lock
this.Calls.Mem2
(fun _ ->
this.Calls.Mem2.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0
}
)
this.Mem2 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem3 ((arg_0_0, arg_0_1)) arg_1_0 =
lock
this.Calls.Mem3
(fun _ ->
this.Calls.Mem3.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0
}
)
this.Mem3 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem4 ((arg_0_0, arg_0_1)) ((arg_1_0, arg_1_1)) =
lock
this.Calls.Mem4
(fun _ ->
this.Calls.Mem4.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem4 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem5 (arg_0_0, arg_0_1) ((arg_1_0, arg_1_1)) =
lock
this.Calls.Mem5
(fun _ ->
this.Calls.Mem5.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem5 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1) =
lock
this.Calls.Mem6
(fun _ ->
this.Calls.Mem6.Add
{
Arg0 = arg_0_0, arg_0_1
Arg1 = arg_1_0, arg_1_1
}
)
this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
namespace SomeNamespace.CapturingMock
open System
[<RequireQualifiedAccess>]
module internal TypeWithInterfaceNoAttrMockCalls =
/// All the calls made to a TypeWithInterfaceNoAttrMock mock
type internal Calls =
{
Mem1 : ResizeArray<string option>
Mem2 : ResizeArray<unit>
}
/// A fresh calls object which has not yet had any calls made.
static member Empty () : Calls =
{
Mem1 = ResizeArray ()
Mem2 = ResizeArray ()
}
/// Mock record type for an interface
type internal TypeWithInterfaceNoAttrMock =
{
Calls : TypeWithInterfaceNoAttrMockCalls.Calls
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
Mem1 : string option -> string[] Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem1_Calls : ResizeArray<string option>
Mem2 : unit -> string[] Async
/// Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it.
Mem2_Calls : ResizeArray<unit>
}
/// An implementation where every non-unit method throws.
static member Empty : TypeWithInterfaceNoAttrMock =
static member Empty () : TypeWithInterfaceNoAttrMock =
{
Calls = TypeWithInterfaceNoAttrMockCalls.Calls.Empty ()
Dispose = (fun () -> ())
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
}
interface TypeWithInterfaceNoAttr with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem2 () = this.Mem2 (())
member this.Mem1 arg_0_0 =
lock this.Calls.Mem1 (fun _ -> this.Calls.Mem1.Add (arg_0_0))
this.Mem1 (arg_0_0)
member this.Mem2 () =
lock this.Calls.Mem2 (fun _ -> this.Calls.Mem2.Add (()))
this.Mem2 (())
interface System.IDisposable with
member this.Dispose () : unit = this.Dispose ()

View File

@@ -10,7 +10,7 @@
<WarnOn>FS3388,FS3559</WarnOn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.38-alpha" PrivateAssets="all" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" PrivateAssets="all" />
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com" />
</ItemGroup>
<PropertyGroup Condition="'$(GITHUB_ACTION)' != ''">

View File

@@ -478,40 +478,51 @@ It takes a type like this:
```fsharp
[<GenerateCapturingMock>]
type IPublicType =
abstract Mem1 : string * int -> string list
abstract Mem1 : string * int -> thing : bool -> string list
abstract Mem2 : baz : string -> unit -> int
```
and stamps out types like this:
```fsharp
[<RequireQualifiedAccess>]
module internal PublicTypeCalls =
type internal Mem2Call =
type internal Mem1Call =
{
baz : string
Arg1 : unit
Arg0 : string * int
thing : bool
}
type internal Calls =
{
Mem1 : ResizeArray<Mem1Call>
Mem2 : ResizeArray<string>
}
static member Empty () = { Mem1 = ResizeArray () ; Mem2 = ResizeArray () }
/// Mock record type for an interface
type internal PublicTypeMock =
{
Mem1 : string * int -> string list
Mem1 : string * int -> bool -> string list
Mem2 : string -> int
Mem2_Calls : ResizeArray<string * int>
Mem2_Calls : ResizeArray<PublicTypeCalls.Mem2Call>
Calls : PublicTypeCalls.Calls
}
static member Empty : PublicTypeMock =
{
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem1_Calls = ResizeArray ()
Mem2_Calls = ResizeArray ()
Calls = PublicTypeMockCalls.Calls.Empty ()
}
interface IPublicType with
member this.Mem1 (arg0, arg1) = this.Mem1 (arg0, arg1)
member this.Mem2 (arg0) = this.Mem2 (arg0)
member this.Mem1 (arg0, arg1) =
lock this.Calls.Mem1 (fun () -> this.Calls.Mem1.Add { Arg0 = arg0 ; thing = arg1 })
this.Mem1 (arg0, arg1)
member this.Mem2 (arg0) =
lock this.Calls.Mem2 (fun () -> this.Calls.Mem2.Add arg0)
this.Mem2 (arg0)
```
### What's the point?
@@ -525,6 +536,36 @@ thereby allowing the programmer to use F#'s record-update syntax.
* You may supply an `isInternal : bool` argument to the attribute. By default, we make the resulting record type at most internal (never public), since this is intended only to be used in tests; but you can instead make it public with `[<GenerateMock false>]`.
### Gotchas (GenerateCapturingMock)
We use the same name for the record field as the implementing interface member:
```fsharp
type FooMock =
{
Field : string -> unit
}
interface IFoo with
member _.Field x = ...
```
If you have an object of type `FooMock` in scope, you'll get the *record field*, not the *interface member*.
You need to cast it to `IFoo` before using it (or pass it into a function which takes an `IFoo`):
```fsharp
let thing = FooMock.Empty () // of type FooMock
thing.Field "hello" // the wrong one! bypasses the IFoo implementation which captures calls
// correct:
let thing' = FooMock.Empty ()
let thing = thing' :> IFoo
thing.Field "hello" // the right one: this call does get recorded in the mock, because this is the interface member
// also correct, but beware because it leaves the chance of the above footgun lying around for later:
let thing = FooMock.Empty () // of type FooMock
doTheThing thing // where doTheThing : IFoo -> unit
```
## `CreateCatamorphism`
Takes a collection of mutually recursive discriminated unions:
@@ -611,13 +652,13 @@ For example, [PureGymDto.fs](./ConsumePlugin/PureGymDto.fs) is a real-world set
* In your `.fsproj` file, define a helper variable so that subsequent steps don't all have to be kept in sync:
```xml
<PropertyGroup>
<WoofWareMyriadPluginVersion>2.0.1</WoofWareMyriadPluginVersion>
<WoofWareMyriadPluginVersion>9.0.1</WoofWareMyriadPluginVersion>
</PropertyGroup>
```
* Take a reference on `WoofWare.Myriad.Plugins.Attributes` (which has no other dependencies), to obtain access to the attributes which the generator will recognise:
```xml
<ItemGroup>
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="2.0.2" />
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.7.2" />
</ItemGroup>
```
* Take a reference (with private assets, to prevent these from propagating to your own assembly) on `WoofWare.Myriad.Plugins`, to obtain the plugins which Myriad will run, and on `Myriad.Sdk`, to obtain the Myriad binary itself:

View File

@@ -100,3 +100,24 @@ type ArgumentFlagAttribute (flagValue : bool) =
[<AttributeUsage(AttributeTargets.Field, AllowMultiple = true)>]
type ArgumentLongForm (s : string) =
inherit Attribute ()
/// Attribute indicating that this boolean or flag field should accept `--no-` prefix for negation.
/// When this attribute is present on a boolean or flag DU field, the generated parser will accept
/// both --field-name and --no-field-name as argument forms.
///
/// For boolean fields:
/// --field-name (or --field-name=true) sets the value to true
/// --no-field-name (or --no-field-name=true) sets the value to false
/// --field-name=false sets the value to false
/// --no-field-name=false sets the value to true
///
/// For flag DU fields with [<ArgumentFlag>]:
/// --field-name (or --field-name=true) sets to the case marked with [<ArgumentFlag true>]
/// --no-field-name (or --no-field-name=true) sets to the case marked with [<ArgumentFlag false>]
/// --field-name=false sets to the [<ArgumentFlag false>] case
/// --no-field-name=false sets to the [<ArgumentFlag true>] case
///
/// This attribute can only be applied to bool fields or flag DU fields (two-case DUs with [<ArgumentFlag>]).
[<AttributeUsage(AttributeTargets.Field, AllowMultiple = false)>]
type ArgumentNegateWithPrefixAttribute () =
inherit Attribute ()

View File

@@ -32,7 +32,11 @@ type GenerateMockAttribute (isInternal : bool) =
/// record update syntax to easily specify partially-implemented mock objects.
/// You may optionally specify `isInternal = false` to get a mock with the public visibility modifier.
///
/// The default implementation of each field captures all calls made to it, which can then be accessed later.
/// The default implementation of each field throws.
///
/// The generated interface methods capture all calls made to them, before passing through to the relevant
/// field of the mock record; the calls can be accessed later through the `Calls` field of the generated
/// mock record.
type GenerateCapturingMockAttribute (isInternal : bool) =
inherit Attribute ()
/// The default value of `isInternal`, the optional argument to the GenerateCapturingMockAttribute constructor.

View File

@@ -13,6 +13,8 @@ WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.ArgumentLongForm inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentLongForm..ctor [constructor]: string
WoofWare.Myriad.Plugins.ArgumentNegateWithPrefixAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.ArgumentNegateWithPrefixAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.CreateCatamorphismAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.CreateCatamorphismAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.GenerateCapturingMockAttribute inherit System.Attribute

View File

@@ -17,10 +17,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="5.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="ApiSurface" Version="5.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0"/>
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,5 @@
{
"version": "3.7",
"version": "3.8",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],

View File

@@ -0,0 +1,372 @@
namespace WoofWare.Myriad.Plugins.Test
open System.Threading
open NUnit.Framework
open FsUnitTyped
open ConsumePlugin
[<TestFixture>]
module TestArgParserNegation =
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo=true sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature=true" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo true sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ; "true" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo=false sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature=false" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --foo false sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--enable-feature" ; "false" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo=true sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature=true" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo true sets to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "true" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo=false sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature=false" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Boolean field with ArgumentNegateWithPrefix: --no-foo false sets to true`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "false" ]
result.EnableFeature |> shouldEqual true
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run=true sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run=true" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run true sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ; "true" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run=false sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run=false" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --dry-run false sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--dry-run" ; "false" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run=true sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run=true" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run true sets to Wet`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ; "true" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run=false sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run=false" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``Flag DU with ArgumentNegateWithPrefix: --no-dry-run false sets to Dry`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--no-dry-run" ; "false" ]
result.DryRun |> shouldEqual TestDryRunMode.Dry
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--verbose" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --v`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--v" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-verbose" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-v`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-v" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --verbose=false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--verbose=false" ]
result.VerboseMode |> shouldEqual false
[<Test>]
let ``ArgumentNegateWithPrefix works with multiple ArgumentLongForm: --no-v=false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = MultipleFormsNegation.parse' getEnvVar [ "--no-v=false" ]
result.VerboseMode |> shouldEqual true
[<Test>]
let ``Help text shows both standard and negated forms for boolean`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> BoolNegation.parse' getEnvVar [ "--help" ] |> ignore<BoolNegation>)
exc.Message
|> shouldEqual
"""Help text requested.
--enable-feature / --no-enable-feature bool"""
[<Test>]
let ``Help text shows both standard and negated forms for flag DU`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> FlagNegation.parse' getEnvVar [ "--help" ] |> ignore<FlagNegation>)
exc.Message
|> shouldEqual
"""Help text requested.
--dry-run / --no-dry-run bool"""
[<Test>]
let ``Help text with multiple long forms shows all variants`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () ->
MultipleFormsNegation.parse' getEnvVar [ "--help" ]
|> ignore<MultipleFormsNegation>
)
exc.Message
|> shouldEqual
"""Help text requested.
--verbose / --v / --no-verbose / --no-v bool"""
[<Test>]
let ``Multiple occurrences error: --foo and --no-foo both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--enable-feature" ; "--no-enable-feature" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``Multiple occurrences error: --no-foo and --foo both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--no-enable-feature" ; "--enable-feature" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``Multiple occurrences error: --foo=true and --no-foo=false both supplied`` () =
let getEnvVar (_ : string) = failwith "should not call"
let exc =
Assert.Throws<exn> (fun () ->
BoolNegation.parse' getEnvVar [ "--enable-feature=true" ; "--no-enable-feature=false" ]
|> ignore<BoolNegation>
)
// Should report as duplicate argument
exc.Message |> shouldContainText "supplied multiple times"
[<Test>]
let ``CombinedFeatures: verbose with default value works with negation`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = CombinedFeatures.parse' getEnvVar [ "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice2Of2 false)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: can override default with --verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result =
CombinedFeatures.parse' getEnvVar [ "--verbose" ; "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice1Of2 true)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: can override default with --no-verbose`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result =
CombinedFeatures.parse' getEnvVar [ "--no-verbose" ; "--debug" ; "--normal-bool" ]
result.Verbose |> shouldEqual (Choice1Of2 false)
result.Debug |> shouldEqual true
result.NormalBool |> shouldEqual true
[<Test>]
let ``CombinedFeatures: --no-debug sets debug to false`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = CombinedFeatures.parse' getEnvVar [ "--no-debug" ; "--normal-bool" ]
result.Debug |> shouldEqual false
[<Test>]
let ``CombinedFeatures: help text shows negation for fields with attribute`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () -> CombinedFeatures.parse' getEnvVar [ "--help" ] |> ignore<CombinedFeatures>)
// Verbose and Debug should have --no- forms, NormalBool should not
exc.Message |> shouldContainText "--verbose / --no-verbose"
exc.Message |> shouldContainText "--debug / --no-debug"
exc.Message |> shouldContainText "--normal-bool bool"
exc.Message |> shouldNotContainText "--no-normal-bool"
[<Test>]
let ``Case insensitivity: --NO-ENABLE-FEATURE works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--NO-ENABLE-FEATURE" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Case insensitivity: --No-Enable-Feature works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = BoolNegation.parse' getEnvVar [ "--No-Enable-Feature" ]
result.EnableFeature |> shouldEqual false
[<Test>]
let ``Case insensitivity: --NO-DRY-RUN works`` () =
let getEnvVar (_ : string) = failwith "should not call"
let result = FlagNegation.parse' getEnvVar [ "--NO-DRY-RUN" ]
result.DryRun |> shouldEqual TestDryRunMode.Wet

View File

@@ -11,7 +11,7 @@ module TestCapturingMockGenerator =
[<Test>]
let ``Example of use: IPublicType`` () =
let mock : IPublicType =
{ PublicTypeMock.Empty with
{ PublicTypeMock.Empty () with
Mem1 = fun (s, count) -> List.replicate count s
}
:> _
@@ -38,7 +38,7 @@ module TestCapturingMockGenerator =
[<Test>]
let ``Example of use: properties`` () =
let mock : TypeWithProperties =
{ TypeWithPropertiesMock.Empty with
{ TypeWithPropertiesMock.Empty () with
Mem1 = fun i -> async { return Option.toArray i }
Prop1 = fun () -> 44
}
@@ -50,7 +50,7 @@ module TestCapturingMockGenerator =
[<Test>]
let ``Example of curried use`` () =
let mock =
let mock' =
{ CurriedMock<string>.Empty () with
Mem1 =
fun x y ->
@@ -59,9 +59,11 @@ module TestCapturingMockGenerator =
"it's me"
}
let mock = mock' :> Curried<_>
mock.Mem1 3 "hello" |> shouldEqual "it's me"
lock mock.Mem1_Calls (fun () -> Seq.toList mock.Mem1_Calls)
lock mock'.Calls.Mem1 (fun () -> Seq.toList mock'.Calls.Mem1)
|> List.exactlyOne
|> shouldEqual
{

View File

@@ -11,7 +11,7 @@ module TestCapturingMockGeneratorNoAttr =
[<Test>]
let ``Example of use: IPublicType`` () =
let mock : IPublicTypeNoAttr =
{ PublicTypeNoAttrMock.Empty with
{ PublicTypeNoAttrMock.Empty () with
Mem1 = fun (s, count) -> List.replicate count s
}
:> _

View File

@@ -38,6 +38,7 @@
<Compile Include="TestCataGenerator\TestMyList.fs" />
<Compile Include="TestCataGenerator\TestMyList2.fs" />
<Compile Include="TestArgParser\TestArgParser.fs" />
<Compile Include="TestArgParser\TestArgParserNegation.fs" />
<Compile Include="TestSwagger\TestSwaggerParse.fs" />
<Compile Include="TestSwagger\TestOpenApi3Parse.fs" />
<EmbeddedResource Include="TestSwagger\api-with-examples.json" />
@@ -55,13 +56,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="5.0.1" />
<PackageReference Include="FsCheck" Version="3.3.1" />
<PackageReference Include="ApiSurface" Version="5.0.3" />
<PackageReference Include="FsCheck" Version="3.3.2" />
<PackageReference Include="FsUnit" Version="7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
<PackageReference Include="WoofWare.Expect" Version="0.8.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="WoofWare.Expect" Version="0.8.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -72,16 +72,38 @@ type private ParseFunction<'acc> =
/// and choices and so on.
TargetType : SynType
Accumulation : 'acc
/// If true, this boolean/flag field accepts --no- prefix for negation (has [<ArgumentNegateWithPrefix>])
AcceptsNegation : bool
}
/// A SynExpr of type `string` which we can display to the user at generated-program runtime to display all
/// the ways they can refer to this arg.
member arg.HumanReadableArgForm : SynExpr =
let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
if arg.AcceptsNegation then
// Include both standard and --no- variants
// E.g., "--foo / --bar / --no-foo / --no-bar"
let standardFormatString =
List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
let negatedFormatString =
List.replicate arg.ArgForm.Length "--no-%s" |> String.concat " / "
let combinedFormatString = standardFormatString + " / " + negatedFormatString
// Apply all arg forms twice (once for standard, once for negated)
let allArgForms = arg.ArgForm @ arg.ArgForm
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst combinedFormatString),
allArgForms)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
else
// Standard behavior: just --foo / --bar
let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
[<RequireQualifiedAccess>]
type private ChoicePositional =
@@ -222,8 +244,8 @@ module private ParseTree =
nonPos @ nonPos2, Some pos
|> fun (nonPos, pos) ->
let duplicateArgs =
// This is best-effort. We can't necessarily detect all SynExprs here, but usually it'll be strings.
// Extract all arg form strings for validation
let allArgForms =
Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm)
|> Seq.concat
|> Seq.choose (fun expr ->
@@ -232,14 +254,57 @@ module private ParseTree =
| _ -> None
)
|> List.ofSeq
// Check for direct duplicates
let duplicateArgs =
allArgForms
|> List.groupBy id
|> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None)
match duplicateArgs with
| [] -> nonPos, pos
| dups ->
| dups when not dups.IsEmpty ->
let dups = dups |> String.concat " "
failwith $"Duplicate args detected! %s{dups}"
| _ ->
// Check for --no- prefix conflicts
// Build a map of arg names that have AcceptsNegation=true
let negatedForms =
nonPos
|> List.filter _.AcceptsNegation
|> List.collect (fun pf ->
pf.ArgForm
|> List.choose (fun expr ->
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String (s, _, _), _) -> Some (pf.FieldName.idText, s)
| _ -> None
)
)
|> List.map (fun (fieldName, argForm) -> $"no-%s{argForm}", fieldName)
|> Map.ofList
// Check if any existing arg form conflicts with a --no- variant
let conflicts =
allArgForms
|> List.choose (fun argForm ->
match negatedForms.TryFind argForm with
| Some fieldWithNegation -> Some (argForm, fieldWithNegation)
| None -> None
)
match conflicts with
| [] -> ()
| conflicts ->
let conflictMessages =
conflicts
|> List.map (fun (argForm, fieldWithNegation) ->
$"Argument name conflict: '--%s{argForm}' collides with the --no- variant of field '%s{fieldWithNegation}' (which has [<ArgumentNegateWithPrefix>])"
)
|> String.concat "\n"
failwith $"Conflicting argument names detected:\n%s{conflictMessages}"
nonPos, pos
/// Build the return value.
let rec instantiate<'a> (tree : ParseTree<'a>) : SynExpr =
@@ -615,6 +680,7 @@ module internal ArgParserGenerator =
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = false
}
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.List Accumulation.Required ->
@@ -627,6 +693,7 @@ module internal ArgParserGenerator =
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = false
}
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.Choice _
@@ -651,6 +718,27 @@ module internal ArgParserGenerator =
Some (Choice2Of2 ())
| parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2
let hasNegateAttr =
attrs
|> List.exists (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (ident, _, _) ->
match (List.last ident).idText with
| "ArgumentNegateWithPrefixAttribute"
| "ArgumentNegateWithPrefix" -> true
| _ -> false
)
let acceptsNegation =
if hasNegateAttr then
match isBoolLike with
| Some _ -> true
| None ->
failwith
$"[<ArgumentNegateWithPrefix>] can only be applied to boolean or flag DU fields, but was applied to field %s{ident.idText} of type %O{fieldType}"
else
false
{
FieldName = ident
Parser = parser
@@ -660,6 +748,7 @@ module internal ArgParserGenerator =
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = acceptsNegation
}
|> fun t -> ParseTree.NonPositionalLeaf (t, Teq.refl)
|> ParseTreeCrate.make
@@ -773,6 +862,37 @@ module internal ArgParserGenerator =
)
|> SynBinding.basic [ Ident.create "helpText" ] [ SynPat.unit ]
/// Helper to create a negated parser for boolean/flag fields.
/// Returns a SynExpr that represents: string -> (negated bool or negated flag DU)
/// For booleans: `fun x -> not (Boolean.Parse x)`
/// For flag DUs: `fun x -> FlagDu.FromBoolean flagDu (not (Boolean.Parse x))`
let private createNegatedParser (arg : ParseFunction<'acc>) : SynExpr =
match arg.BoolCases with
| None -> failwith $"LOGIC ERROR: createNegatedParser called on non-boolean field %s{arg.FieldName.idText}"
| Some (Choice2Of2 ()) ->
// Boolean: parse and negate
// fun x -> not (System.Boolean.Parse x)
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> SynExpr.createLambda "x"
| Some (Choice1Of2 flagDu) ->
// Flag DU: parse as bool, negate, then convert to flag DU
// fun x -> x |> System.Boolean.Parse |> not |> FlagDu.FromBoolean flagDu
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> FlagDu.FromBoolean flagDu
|> SynExpr.createLambda "x"
/// `let processKeyValue (key : string) (value : string) : Result<unit, string option> = ...`
/// Returns a possible error.
/// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do
@@ -786,109 +906,209 @@ module internal ArgParserGenerator =
let args =
args
|> List.map (fun arg ->
match arg.Accumulation with
| Accumulation.Required
| Accumulation.Choice _
| Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s")
|> SynExpr.applyTo arg.HumanReadableArgForm
|> SynExpr.applyTo (SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let assignmentExpr =
match arg.Accumulation with
| Accumulation.Required
| Accumulation.Choice _
| Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (
SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s"
)
|> SynExpr.applyTo arg.HumanReadableArgForm
|> SynExpr.applyTo (
SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let performAssignment =
let performAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
|> fun expr -> arg.ArgForm, expr
// Return (argForms, assignmentExpr), argMetadata
(arg.ArgForm, assignmentExpr), Some arg
)
let posArg =
match pos with
| None -> []
| Some pos ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction pos.Parser
|> fun p ->
match pos.Accumulation with
| ChoicePositional.Choice _ ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal _ -> p
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
|> fun expr -> pos.ArgForm, expr
|> List.singleton
let posExpr =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction pos.Parser
|> fun p ->
match pos.Accumulation with
| ChoicePositional.Choice _ ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal _ -> p
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
// Positional args don't support negation, so metadata is None
[ (pos.ArgForm, posExpr), None ]
(SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), posArg @ args)
||> List.fold (fun finalBranch (argForm, arg) ->
||> List.fold (fun finalBranch ((argForm, arg), argMetadata) ->
(finalBranch, argForm)
||> List.fold (fun finalBranch argForm ->
arg
|> SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
// Standard match: --argForm
let standardMatch =
arg
|> SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
finalBranch
// If this arg accepts negation, also match --no-argForm
match argMetadata with
| None -> standardMatch
| Some (parseFn : ParseFunctionNonPositional) when parseFn.AcceptsNegation ->
// Create negated assignment (same structure as `arg` but with negated parser)
let negatedParser = createNegatedParser parseFn
let negatedArg =
match parseFn.Accumulation with
| Accumulation.Required
| Accumulation.Choice _
| Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (
SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s"
)
|> SynExpr.applyTo parseFn.HumanReadableArgForm
|> SynExpr.applyTo (
SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let performNegatedAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction negatedParser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI parseFn.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
finalBranch
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performNegatedAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' parseFn.TargetVariable)
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction negatedParser
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ parseFn.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
| _ ->
failwith
"WoofWare.Myriad invariant violated: unexpected accumulation type for negated arg"
// Match --no-argForm
negatedArg
|> SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--no-%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
standardMatch
| Some _ -> standardMatch
)
)
|> SynBinding.basic
@@ -909,6 +1129,34 @@ module internal ArgParserGenerator =
|> PreXmlDoc.create'
)
/// Try to extract a constant bool value from a SynExpr.
/// Returns Some true/false if the expr is a constant bool, None otherwise.
let private tryGetConstBool (expr : SynExpr) : bool option =
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.Bool v, _) -> Some v
| _ -> None
/// Helper to get the "false" case for a boolean/flag field.
/// For booleans: `false`
/// For flag DUs: the case marked with [<ArgumentFlag false>]
let private getFalseCase (flag : ParseFunction<'a>) : SynExpr =
match flag.BoolCases with
| None -> failwith $"LOGIC ERROR: getFalseCase called on non-boolean field %s{flag.FieldName.idText}"
| Some (Choice2Of2 ()) ->
// Boolean: return false
SynExpr.CreateConst false
| Some (Choice1Of2 flagDu) ->
// Flag DU: return the case associated with false
// Check which case has false as its arg
match tryGetConstBool flagDu.Case1Arg, tryGetConstBool flagDu.Case2Arg with
| Some false, _ -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ]
| _, Some false -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ]
| Some true, _ -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ]
| _, Some true -> SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ]
| None, None ->
// Can't determine at compile time, use FlagDu.FromBoolean with false
FlagDu.FromBoolean flagDu (SynExpr.CreateConst false)
/// `let setFlagValue (key : string) : bool = ...`
/// The second member of the `flags` list tuple is the constant "true" with which we will interpret the
/// arity-0 `--foo`. So in the case of a boolean-typed field, this is `true`; in the case of a Flag-typed field,
@@ -949,21 +1197,72 @@ module internal ArgParserGenerator =
(finalExpr, flag.ArgForm)
||> List.fold (fun finalExpr argForm ->
SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
finalExpr
matchFlag
// Standard match: --argForm sets to trueCase
let standardMatch =
SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
finalExpr
matchFlag
// If this flag accepts negation, also match --no-argForm sets to falseCase
if flag.AcceptsNegation then
let falseCase = getFalseCase flag
let matchNegatedFlag =
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
// This is an error, but it's one we can gracefully report at the end.
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.CreateConst true
])
SynMatchClause.create
(SynPat.named "None")
([
SynExpr.assign
(SynLongIdent.createI flag.TargetVariable)
(SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") falseCase)
SynExpr.CreateConst true
]
|> SynExpr.sequential)
]
|> SynExpr.createMatch (SynExpr.createIdent' flag.TargetVariable)
// Match --no-argForm
SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction
(SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "--no-%s"))
argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
standardMatch
matchNegatedFlag
else
standardMatch
)
)
|> SynBinding.basic [ Ident.create "setFlagValue" ] [ SynPat.annotateType SynType.string (SynPat.named "key") ]

View File

@@ -50,7 +50,12 @@ module internal CapturingInterfaceMockGenerator =
Attrs = []
Type =
tupledArg.Args
|> List.map (fun pi -> pi.Type)
|> List.map (fun pi ->
if pi.IsOptional then
pi.Type |> SynType.appPostfix "option"
else
pi.Type
)
|> SynType.tupleNoParen
|> Option.get
}
@@ -75,7 +80,7 @@ module internal CapturingInterfaceMockGenerator =
let private buildType (x : ParameterInfo) : SynType =
if x.IsOptional then
SynType.app "option" [ x.Type ]
SynType.appPostfix "option" x.Type
else
x.Type
@@ -139,7 +144,12 @@ module internal CapturingInterfaceMockGenerator =
| [] -> failwith "expected args in member"
| [ ty ] ->
ty.Args
|> List.map _.Type
|> List.map (fun pi ->
if pi.IsOptional then
SynType.appPostfix "option" pi.Type
else
pi.Type
)
|> SynType.tupleNoParen
|> Option.get
|> CallField.Original
@@ -214,8 +224,9 @@ module internal CapturingInterfaceMockGenerator =
| None -> failwith $"unexpectedly got a field with no identifier: %O{f}"
| Some idOpt -> idOpt.idText
f, extraType, fieldName
fieldName, (f, extraType)
)
|> Map.ofList
let failwithNotImplemented (fieldName : string) =
let failString = SynExpr.CreateConst $"Unimplemented mock function: %s{fieldName}"
@@ -248,28 +259,21 @@ module internal CapturingInterfaceMockGenerator =
let originalMembers =
fields
|> List.map (fun (_, _, fieldName) -> SynLongIdent.createS fieldName, failwithNotImplemented fieldName)
|> Map.toList
|> List.map (fun (fieldName, _) -> SynLongIdent.createS fieldName, failwithNotImplemented fieldName)
let callsArrays =
fields
|> List.map (fun (_field, _, fieldName) ->
let name = SynLongIdent.createS $"%s{fieldName}_Calls"
let callsObject =
SynLongIdent.createS "Calls",
SynExpr.applyFunction
(SynExpr.createLongIdent [ $"%s{name}Calls" ; "Calls" ; "Empty" ])
(SynExpr.CreateConst ())
let init =
SynExpr.createIdent "ResizeArray" |> SynExpr.applyTo (SynExpr.CreateConst ())
name, init
)
interfaceExtras @ originalMembers @ callsArrays
callsObject :: interfaceExtras @ originalMembers
let staticMemberEmpty =
SynBinding.basic
[ Ident.create "Empty" ]
(if interfaceType.Generics.IsNone then
[]
else
[ SynPat.unit ])
[ SynPat.unit ]
(SynExpr.createRecord None emptyRecordFieldInstantiations)
|> SynBinding.withXmlDoc (PreXmlDoc.create "An implementation where every non-unit method throws.")
|> SynBinding.withReturnAnnotation constructorReturnType
@@ -290,43 +294,109 @@ module internal CapturingInterfaceMockGenerator =
[]
let nonExtras =
fields |> Map.toSeq |> Seq.map (fun (_, (field, _)) -> field) |> Seq.toList
let calls =
let ty =
match interfaceType.Generics with
| None -> SynType.createLongIdent' [ $"%s{name}Calls" ; "Calls" ]
| Some generics ->
generics.TyparDecls
|> List.map (fun (SynTyparDecl (_, typar)) -> SynType.var typar)
|> SynType.app' (SynType.createLongIdent' [ $"%s{name}Calls" ; "Calls" ])
{
Attrs = []
Ident = Ident.create "Calls" |> Some
Type = ty
}
|> SynField.make
calls :: extras @ nonExtras
let access =
match interfaceType.Accessibility, spec.IsInternal with
| Some (SynAccess.Public _), true
| None, true -> SynAccess.Internal range0
| Some (SynAccess.Public _), false -> SynAccess.Public range0
| None, false -> SynAccess.Public range0
| Some (SynAccess.Internal _), _ -> SynAccess.Internal range0
| Some (SynAccess.Private _), _ -> SynAccess.Private range0
let accessAtLeastInternal =
match access with
| SynAccess.Private _ -> SynAccess.Internal range0
| access -> access
let callsObject =
let fields' =
fields
|> List.collect (fun (field, callType, fieldName) ->
let callField =
match callType with
| CallField.Original ty ->
{
Attrs = []
Ident = Some (fieldName + "_Calls" |> Ident.create)
Type = SynType.app "ResizeArray" [ ty ]
}
|> SynField.make
|> SynField.withDocString (
PreXmlDoc.create
"Additions to this ResizeArray are locked on itself. For maximum safety, lock on this field before reading it."
)
| CallField.ArgsObject (argsObjectName, _, generics) ->
{
Attrs = []
Ident = Some (fieldName + "_Calls" |> Ident.create)
Type =
match generics with
| None -> SynType.named argsObjectName.idText
| Some generics ->
generics.TyparDecls
|> List.map (fun (SynTyparDecl.SynTyparDecl (_, typar)) -> SynType.var typar)
|> SynType.app' (
SynType.createLongIdent' [ $"%s{name}Calls" ; argsObjectName.idText ]
)
|> List.singleton
|> SynType.app "ResizeArray"
}
|> SynField.make
[ field ; callField ]
|> Map.toSeq
|> Seq.map (fun (fieldName, (_, callType)) ->
match callType with
| CallField.Original ty ->
{
Attrs = []
Ident = Some (fieldName |> Ident.create)
Type = SynType.app "ResizeArray" [ ty ]
}
|> SynField.make
| CallField.ArgsObject (argsObjectName, _, generics) ->
{
Attrs = []
Ident = Some (fieldName |> Ident.create)
Type =
match generics with
| None -> SynType.named argsObjectName.idText
| Some generics ->
generics.TyparDecls
|> List.map (fun (SynTyparDecl.SynTyparDecl (_, typar)) -> SynType.var typar)
|> SynType.app' (SynType.createLongIdent' [ argsObjectName.idText ])
|> List.singleton
|> SynType.app "ResizeArray"
}
|> SynField.make
)
|> Seq.toList
extras @ nonExtras
let emptyMember =
let returnType =
match interfaceType.Generics with
| None -> SynType.named "Calls"
| Some generics ->
let generics =
match generics with
| SynTyparDecls.PostfixList (decls = decls)
| SynTyparDecls.PrefixList (decls = decls) -> decls
| SynTyparDecls.SinglePrefix (decl = decl) -> [ decl ]
|> List.map (fun (SynTyparDecl.SynTyparDecl (_, typar)) -> SynType.var typar)
SynType.app "Calls" generics
fields
|> Map.toSeq
|> Seq.map (fun (name, _) ->
SynLongIdent.createS name,
SynExpr.applyFunction (SynExpr.createIdent "ResizeArray") (SynExpr.CreateConst ())
)
|> Seq.toList
|> SynExpr.createRecord None
|> SynBinding.basic [ Ident.create "Empty" ] [ SynPat.unit ]
|> SynBinding.withXmlDoc (PreXmlDoc.create "A fresh calls object which has not yet had any calls made.")
|> SynBinding.withReturnAnnotation returnType
|> SynMemberDefn.staticMember
{
RecordType.Name = Ident.create "Calls"
Fields = fields'
Members = Some [ emptyMember ]
XmlDoc = PreXmlDoc.create $"All the calls made to a %s{name} mock" |> Some
Generics = interfaceType.Generics
TypeAccessibility = Some accessAtLeastInternal
ImplAccessibility = None
Attributes = [ SynAttribute.requireQualifiedAccess ]
}
|> AstHelper.defineRecordType
let interfaceMembers =
let members =
@@ -350,28 +420,65 @@ module internal CapturingInterfaceMockGenerator =
|> fun i -> if tupledArgs.HasParen then SynPat.paren i else i
)
let body =
let tuples =
let body, addToCalls =
let tupleContents =
memberInfo.Args
|> List.mapi (fun i args ->
args.Args
|> List.mapi (fun j arg ->
match arg.Type with
| UnitType -> SynExpr.CreateConst ()
| _ -> SynExpr.createIdent $"arg_%i{i}_%i{j}"
| UnitType -> SynExpr.CreateConst (), arg.Id
| _ -> SynExpr.createIdent $"arg_%i{i}_%i{j}", arg.Id
)
|> SynExpr.tuple
)
let tuples = tupleContents |> List.map (List.map fst >> SynExpr.tuple)
match tuples |> List.rev with
| [] -> failwith "expected args but got none"
| last :: rest ->
(last, rest)
||> List.fold SynExpr.applyTo
|> SynExpr.applyFunction (
SynExpr.createLongIdent' [ Ident.create "this" ; memberInfo.Identifier ]
)
let tuples = (last, rest) ||> List.fold SynExpr.applyTo
let body =
tuples
|> SynExpr.applyFunction (
SynExpr.createLongIdent' [ Ident.create "this" ; memberInfo.Identifier ]
)
let addToCalls =
match Map.tryFind memberInfo.Identifier.idText fields with
| None ->
failwith
$"unexpectedly looking up a nonexistent field %s{memberInfo.Identifier.idText}"
| Some (_, result) ->
match result with
| CallField.Original _ -> tuples
| CallField.ArgsObject _ ->
tupleContents
|> List.mapi (fun i fields ->
match fields with
| [ contents, Some ident ] -> SynLongIdent.create [ ident ], contents
| [ contents, None ] -> SynLongIdent.createS $"Arg%i{i}", contents
| _ ->
SynLongIdent.createS $"Arg%i{i}",
SynExpr.tupleNoParen (fields |> List.map fst)
)
|> SynExpr.createRecord None
|> SynExpr.applyFunction (
SynExpr.createLongIdent [ "this" ; "Calls" ; memberInfo.Identifier.idText ; "Add" ]
)
|> SynExpr.createLambda "_"
|> SynExpr.applyFunction (
SynExpr.createIdent "lock"
|> SynExpr.applyTo (
SynExpr.createLongIdent [ "this" ; "Calls" ; memberInfo.Identifier.idText ]
)
)
body, addToCalls
let body = [ addToCalls ; body ] |> SynExpr.sequential
SynBinding.basic [ Ident.create "this" ; memberInfo.Identifier ] headArgs body
|> SynMemberDefn.memberImplementation
@@ -403,15 +510,6 @@ module internal CapturingInterfaceMockGenerator =
SynMemberDefn.Interface (interfaceName, Some range0, Some (members @ properties), range0)
let access =
match interfaceType.Accessibility, spec.IsInternal with
| Some (SynAccess.Public _), true
| None, true -> SynAccess.Internal range0
| Some (SynAccess.Public _), false -> SynAccess.Public range0
| None, false -> SynAccess.Public range0
| Some (SynAccess.Internal _), _ -> SynAccess.Internal range0
| Some (SynAccess.Private _), _ -> SynAccess.Private range0
let extraInterfaces =
inherits
|> Seq.map (fun inheritance ->
@@ -448,22 +546,23 @@ module internal CapturingInterfaceMockGenerator =
let typeDecl = AstHelper.defineRecordType record
let callsModule =
fields
|> List.choose (fun (_, field, _) ->
match field with
| CallField.Original _ -> None
| CallField.ArgsObject (_, callType, _) -> Some callType
let types =
fields
|> Map.toSeq
|> Seq.choose (fun (_, (_, field)) ->
match field with
| CallField.Original _ -> None
| CallField.ArgsObject (_, callType, _) -> Some (SynModuleDecl.Types ([ callType ], range0))
)
|> Seq.toList
types @ [ SynModuleDecl.Types ([ callsObject ], range0) ]
|> SynModuleDecl.nestedModule (
SynComponentInfo.create (Ident.create $"%s{name}Calls")
|> SynComponentInfo.withAccessibility accessAtLeastInternal
|> SynComponentInfo.addAttributes [ SynAttribute.requireQualifiedAccess ]
)
|> function
| [] -> None
| l ->
SynModuleDecl.Types (l, range0)
|> List.singleton
|> SynModuleDecl.nestedModule (
SynComponentInfo.create (Ident.create $"%s{name}Calls")
|> SynComponentInfo.withAccessibility access
)
|> Some
|> Some
(callsModule, SynModuleDecl.Types ([ typeDecl ], range0))

View File

@@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="Myriad.Core" Version="0.8.3" />
<PackageReference Include="TypeEquality" Version="0.3.0" />
<PackageReference Include="WoofWare.Whippet.Fantomas" Version="0.6.3" />
<PackageReference Include="TypeEquality" Version="0.4.2" />
<PackageReference Include="WoofWare.Whippet.Fantomas" Version="0.6.4" />
<!-- the lowest version allowed by Myriad.Core -->
<PackageReference Update="FSharp.Core" Version="6.0.1" PrivateAssets="all"/>
</ItemGroup>

View File

@@ -1,5 +1,5 @@
{
"version": "8.1",
"version": "9.1",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
@@ -11,4 +11,4 @@
":/README.md",
":/Directory.Build.props"
]
}
}

View File

@@ -10,7 +10,8 @@
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.17.0]" />
<PackageDownload Include="WoofWare.FSharpAnalyzers" Version="[0.2.2]" />
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.20.0]" />
</ItemGroup>
</Project>

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1757746433,
"narHash": "sha256-fEvTiU4s9lWgW7mYEU/1QUPirgkn+odUBTaindgiziY=",
"lastModified": 1763191728,
"narHash": "sha256-esRhOS0APE6k40Hs/jjReXg+rx+J5LkWw7cuWFKlwYA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d7ec06d6868ac6d94c371458fc2391ded9ff13d",
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
"type": "github"
},
"original": {

View File

@@ -1,8 +1,8 @@
[
{
"pname": "ApiSurface",
"version": "5.0.1",
"hash": "sha256-0GMXEMFgWbbE2OGxW+6h4zGgQHg+IZy1aI13Dn97xSU="
"version": "5.0.3",
"hash": "sha256-uU5mqLL6zMt17oPYMzhB57ryYC6O6FzSjmdTFg7LvNo="
},
{
"pname": "fantomas",
@@ -26,13 +26,13 @@
},
{
"pname": "FsCheck",
"version": "3.3.1",
"hash": "sha256-k65ksdOSOGz+meRUUND+yuqJtm5ChaKuaxmRIdKzx2Y="
"version": "3.3.2",
"hash": "sha256-3ydyTGpqySynjbcWbmFVeCBnT3KDH3miPSJYJlyxrGs="
},
{
"pname": "fsharp-analyzers",
"version": "0.32.1",
"hash": "sha256-le6rPnAF7cKGBZ2w8H2u9glK+6rT2ZjiAVnrkH2IhrM="
"version": "0.34.1",
"hash": "sha256-Y6PzfVGob2EgX29ZhZIde5EhiZ28Y1+U2pJ6ybIsHV0="
},
{
"pname": "FSharp.Core",
@@ -61,8 +61,8 @@
},
{
"pname": "Microsoft.ApplicationInsights",
"version": "2.22.0",
"hash": "sha256-mUQ63atpT00r49ca50uZu2YCiLg3yd6r3HzTryqcuEA="
"version": "2.23.0",
"hash": "sha256-5sf3bg7CZZjHseK+F3foOchEhmVeioePxMZVvS6Rjb0="
},
{
"pname": "Microsoft.AspNetCore.App.Ref",
@@ -91,13 +91,13 @@
},
{
"pname": "Microsoft.CodeCoverage",
"version": "17.14.1",
"hash": "sha256-f8QytG8GvRoP47rO2KEmnDLxIpyesaq26TFjDdW40Gs="
"version": "18.0.1",
"hash": "sha256-G6y5iyHZ3R2shlLCW/uTusio/UqcnWT79X+UAbxvDQY="
},
{
"pname": "Microsoft.NET.Test.Sdk",
"version": "17.14.1",
"hash": "sha256-mZUzDFvFp7x1nKrcnRd0hhbNu5g8EQYt8SKnRgdhT/A="
"version": "18.0.1",
"hash": "sha256-0c3/rp9di0w7E5UmfRh6Prrm3Aeyi8NOj5bm2i6jAh0="
},
{
"pname": "Microsoft.NETCore.App.Host.linux-arm64",
@@ -166,43 +166,48 @@
},
{
"pname": "Microsoft.Testing.Extensions.Telemetry",
"version": "1.5.3",
"hash": "sha256-bIXwPSa3jkr2b6xINOqMUs6/uj/r4oVFM7xq3uVIZDU="
"version": "1.9.0",
"hash": "sha256-JT91ThKLEyoRS/8ZJqZwlSTT7ofC2QhNqPFI3pYmMaw="
},
{
"pname": "Microsoft.Testing.Extensions.TrxReport.Abstractions",
"version": "1.5.3",
"hash": "sha256-IfMRfcyaIKEMRtx326ICKtinDBEfGw/Sv8ZHawJ96Yc="
"version": "1.9.0",
"hash": "sha256-oscZOEKw7gM6eRdDrOS3x+CwqIvXWRmfmi0ugCxBRw0="
},
{
"pname": "Microsoft.Testing.Extensions.VSTestBridge",
"version": "1.5.3",
"hash": "sha256-XpM/yFjhLSsuzyDV+xKubs4V1zVVYiV05E0+N4S1h0g="
"version": "1.9.0",
"hash": "sha256-CadXLWD093sUDaWhnppzD9LvpxSRqqt93ZEOFiIAPyw="
},
{
"pname": "Microsoft.Testing.Platform",
"version": "1.5.3",
"hash": "sha256-y61Iih6w5D79dmrj2V675mcaeIiHoj1HSa1FRit2BLM="
"version": "1.9.0",
"hash": "sha256-6nzjoYbJOh7v/GB7d+TDuM0l/xglCshFX6KWjg7+cFI="
},
{
"pname": "Microsoft.Testing.Platform.MSBuild",
"version": "1.5.3",
"hash": "sha256-YspvjE5Jfi587TAfsvfDVJXNrFOkx1B3y1CKV6m7YLY="
"version": "1.9.0",
"hash": "sha256-/bileP4b+9RZp8yjgS6eynXwc2mohyyzf6p/0LZJd8I="
},
{
"pname": "Microsoft.TestPlatform.AdapterUtilities",
"version": "17.13.0",
"hash": "sha256-Vr+3Tad/h/nk7f/5HMExn3HvCGFCarehFAzJSfCBaOc="
},
{
"pname": "Microsoft.TestPlatform.ObjectModel",
"version": "17.12.0",
"hash": "sha256-3XBHBSuCxggAIlHXmKNQNlPqMqwFlM952Av6RrLw1/w="
"version": "17.13.0",
"hash": "sha256-6S0fjfj8vA+h6dJVNwLi6oZhYDO/I/6hBZaq2VTW+Uk="
},
{
"pname": "Microsoft.TestPlatform.ObjectModel",
"version": "17.14.1",
"hash": "sha256-QMf6O+w0IT+16Mrzo7wn+N20f3L1/mDhs/qjmEo1rYs="
"version": "18.0.1",
"hash": "sha256-oJbS7SZ46RzyOQ+gCysW7qJRy7V8RlQVa5d8uajb91M="
},
{
"pname": "Microsoft.TestPlatform.TestHost",
"version": "17.14.1",
"hash": "sha256-1cxHWcvHRD7orQ3EEEPPxVGEkTpxom1/zoICC9SInJs="
"version": "18.0.1",
"hash": "sha256-OXYf5vg4piDr10ve0bZ2ZSb+nb3yOiHayJV3cu5sMV4="
},
{
"pname": "Myriad.Core",
@@ -216,8 +221,8 @@
},
{
"pname": "Nerdbank.GitVersioning",
"version": "3.8.38-alpha",
"hash": "sha256-gPMrVbjOZxXoofczF/pn6eVkLhjVSJIyQrLO2oljrDc="
"version": "3.9.50",
"hash": "sha256-BiBfXwr8ob2HTaFk2L5TwAgtvd/EPoqudSI9nhAjQPI="
},
{
"pname": "NETStandard.Library",
@@ -271,8 +276,8 @@
},
{
"pname": "NUnit3TestAdapter",
"version": "5.0.0",
"hash": "sha256-7jZM4qAbIzne3AcdFfMbvbgogqpxvVe6q2S7Ls8xQy0="
"version": "5.2.0",
"hash": "sha256-ybTutL4VkX/fq61mS+O3Ruh+adic4fpv+MKgQ0IZvGg="
},
{
"pname": "RestEase",
@@ -376,17 +381,17 @@
},
{
"pname": "TypeEquality",
"version": "0.3.0",
"hash": "sha256-V50xAOzzyUJrY+MYPRxtnqW5MVeATXCes89wPprv1r4="
"version": "0.4.2",
"hash": "sha256-YxK6BGHjcuP76j5BbTDi814jxGqOevQSgS00IJcjZSA="
},
{
"pname": "WoofWare.Expect",
"version": "0.8.2",
"hash": "sha256-iqt4FPkUr24/4dnRfh7P5nPNNlaPzaItP6/yrGRrQyo="
"version": "0.8.4",
"hash": "sha256-UI7f2nt4g4Gg1Ke/IChrA4fpVOYAChXpvR6zkKfkmzE="
},
{
"pname": "WoofWare.Whippet.Fantomas",
"version": "0.6.3",
"hash": "sha256-FkW/HtVp8/HE2k6d7yFpnJcnP3FNNe9kGlkoIWmNgDw="
"version": "0.6.4",
"hash": "sha256-ScZ7EEcxLvXyam2ZVqDK58QlK3RcePWghzRvtLLLdZI="
}
]