From c3af52596ff4e8ce459b617a860e3f75cfc684b2 Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:58:30 +0000 Subject: [PATCH] Permit public mocks (#94) --- ConsumePlugin/GeneratedMock.fs | 43 ++++++++++++++++ ConsumePlugin/MockExample.fs | 11 +++++ .../Attributes.fs | 8 ++- .../SurfaceBaseline.txt | 3 ++ .../version.json | 2 +- .../InterfaceMockGenerator.fs | 49 ++++++++++++++----- 6 files changed, 103 insertions(+), 13 deletions(-) diff --git a/ConsumePlugin/GeneratedMock.fs b/ConsumePlugin/GeneratedMock.fs index 625b1f1..af0142c 100644 --- a/ConsumePlugin/GeneratedMock.fs +++ b/ConsumePlugin/GeneratedMock.fs @@ -30,6 +30,29 @@ namespace SomeNamespace open WoofWare.Myriad.Plugins +/// Mock record type for an interface +type public PublicTypeInternalFalseMock = + { + Mem1 : string * int -> string list + Mem2 : string -> int + Mem3 : int * option -> string + } + + static member Empty : PublicTypeInternalFalseMock = + { + Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) + Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) + Mem3 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) + } + + 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) +namespace SomeNamespace + +open WoofWare.Myriad.Plugins + /// Mock record type for an interface type internal InternalTypeMock = { @@ -70,6 +93,26 @@ namespace SomeNamespace open WoofWare.Myriad.Plugins +/// Mock record type for an interface +type private PrivateTypeInternalFalseMock = + { + Mem1 : string * int -> unit + Mem2 : string -> int + } + + static member Empty : PrivateTypeInternalFalseMock = + { + Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) + Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) + } + + 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) +namespace SomeNamespace + +open WoofWare.Myriad.Plugins + /// Mock record type for an interface type internal VeryPublicTypeMock<'a, 'b> = { diff --git a/ConsumePlugin/MockExample.fs b/ConsumePlugin/MockExample.fs index 35930c5..02bdbf4 100644 --- a/ConsumePlugin/MockExample.fs +++ b/ConsumePlugin/MockExample.fs @@ -8,6 +8,12 @@ type IPublicType = abstract Mem2 : string -> int abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string +[] +type IPublicTypeInternalFalse = + abstract Mem1 : string * int -> string list + abstract Mem2 : string -> int + abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string + [] type internal InternalType = abstract Mem1 : string * int -> unit @@ -18,6 +24,11 @@ type private PrivateType = abstract Mem1 : string * int -> unit abstract Mem2 : string -> int +[] +type private PrivateTypeInternalFalse = + abstract Mem1 : string * int -> unit + abstract Mem2 : string -> int + [] type VeryPublicType<'a, 'b> = abstract Mem1 : 'a -> 'b diff --git a/WoofWare.Myriad.Plugins.Attributes/Attributes.fs b/WoofWare.Myriad.Plugins.Attributes/Attributes.fs index 1e2546d..b39bf83 100644 --- a/WoofWare.Myriad.Plugins.Attributes/Attributes.fs +++ b/WoofWare.Myriad.Plugins.Attributes/Attributes.fs @@ -13,8 +13,14 @@ type RemoveOptionsAttribute () = /// This generator creates a record which implements the interface, /// but where each method is represented as a record field, so you can use /// record update syntax to easily specify partially-implemented mock objects. -type GenerateMockAttribute () = +/// You may optionally specify `isInternal = false` to get a mock with the public visibility modifier. +type GenerateMockAttribute (isInternal : bool) = inherit Attribute () + /// The default value of `isInternal`, the optional argument to the GenerateMockAttribute constructor. + static member DefaultIsInternal = true + + /// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details. + new () = GenerateMockAttribute GenerateMockAttribute.DefaultIsInternal /// Attribute indicating a record type to which the "Add JSON serializer" Myriad /// generator should apply during build. diff --git a/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt b/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt index 2e08fdf..c98a9d4 100644 --- a/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt +++ b/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt @@ -1,5 +1,8 @@ WoofWare.Myriad.Plugins.GenerateMockAttribute inherit System.Attribute +WoofWare.Myriad.Plugins.GenerateMockAttribute..ctor [constructor]: bool WoofWare.Myriad.Plugins.GenerateMockAttribute..ctor [constructor]: unit +WoofWare.Myriad.Plugins.GenerateMockAttribute.DefaultIsInternal [static property]: [read-only] bool +WoofWare.Myriad.Plugins.GenerateMockAttribute.get_DefaultIsInternal [static method]: unit -> bool WoofWare.Myriad.Plugins.HttpClientAttribute inherit System.Attribute WoofWare.Myriad.Plugins.HttpClientAttribute..ctor [constructor]: unit WoofWare.Myriad.Plugins.JsonParseAttribute inherit System.Attribute diff --git a/WoofWare.Myriad.Plugins.Attributes/version.json b/WoofWare.Myriad.Plugins.Attributes/version.json index 9c50a89..a2ebf80 100644 --- a/WoofWare.Myriad.Plugins.Attributes/version.json +++ b/WoofWare.Myriad.Plugins.Attributes/version.json @@ -1,5 +1,5 @@ { - "version": "2.0", + "version": "2.1", "publicReleaseRefSpec": [ "^refs/heads/main$" ], diff --git a/WoofWare.Myriad.Plugins/InterfaceMockGenerator.fs b/WoofWare.Myriad.Plugins/InterfaceMockGenerator.fs index 20988b8..20d620b 100644 --- a/WoofWare.Myriad.Plugins/InterfaceMockGenerator.fs +++ b/WoofWare.Myriad.Plugins/InterfaceMockGenerator.fs @@ -6,6 +6,11 @@ open Fantomas.FCS.SyntaxTrivia open Fantomas.FCS.Xml open Myriad.Core +type internal GenerateMockOutputSpec = + { + IsInternal : bool + } + [] module internal InterfaceMockGenerator = open Fantomas.FCS.Text.Range @@ -17,6 +22,7 @@ module internal InterfaceMockGenerator = | Some id -> id let createType + (spec : GenerateMockOutputSpec) (name : string) (interfaceType : InterfaceType) (xmlDoc : PreXmlDoc) @@ -249,13 +255,14 @@ module internal InterfaceMockGenerator = SynMemberDefn.Interface (interfaceName, Some range0, Some members, range0) - // TODO: allow an arg to the attribute, specifying a custom visibility let access = - match interfaceType.Accessibility with - | Some (SynAccess.Public _) - | Some (SynAccess.Internal _) - | None -> SynAccess.Internal range0 - | Some (SynAccess.Private _) -> SynAccess.Private range0 + 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 record = { @@ -307,7 +314,7 @@ module internal InterfaceMockGenerator = let createRecord (namespaceId : LongIdent) (opens : SynOpenDeclTarget list) - (interfaceType : SynTypeDefn) + (interfaceType : SynTypeDefn, spec : GenerateMockOutputSpec) : SynModuleOrNamespace = let interfaceType = AstHelper.parseInterface interfaceType @@ -324,7 +331,7 @@ module internal InterfaceMockGenerator = s |> fun s -> s + "Mock" - let typeDecl = createType name interfaceType docString fields + let typeDecl = createType spec name interfaceType docString fields SynModuleOrNamespace.CreateNamespace ( @@ -349,9 +356,29 @@ type InterfaceMockGenerator () = let namespaceAndInterfaces = types |> List.choose (fun (ns, types) -> - match types |> List.filter Ast.hasAttribute with - | [] -> None - | types -> Some (ns, types) + types + |> List.choose (fun typeDef -> + match Ast.getAttribute typeDef with + | None -> None + | Some attr -> + let arg = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.Const (SynConst.Bool value, _) -> value + | SynExpr.Const (SynConst.Unit, _) -> GenerateMockAttribute.DefaultIsInternal + | arg -> + failwith + $"Unrecognised argument %+A{arg} to [<%s{nameof GenerateMockAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only." + + let spec = + { + IsInternal = arg + } + + Some (typeDef, spec) + ) + |> function + | [] -> None + | ty -> Some (ns, ty) ) let opens = AstHelper.extractOpens ast