From 6b670d952cd0711eae4efc6e2f57ef688c0c9b4f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 21 Mar 2024 16:34:50 +0100 Subject: [PATCH] Finish json for shared types :sparkles: --- src/Json/ARCtrl.Json.fsproj | 3 +- src/Json/Comment.fs | 6 +- src/Json/Decode.fs | 13 + src/Json/Encode.fs | 5 + src/Json/OntologyAnnotation.fs | 6 +- src/Json/OntologySourceReference.fs | 52 +-- src/Json/Person.fs | 308 ++++++++++++------ src/Json/Publication.fs | 153 ++++++--- .../context/rocrate/isa_person_context.fs | 2 +- tests/Json/ARCtrl.Json.Tests.fsproj | 1 + tests/Json/Decoder.Tests.fs | 58 ++++ tests/Json/Main.fs | 1 + 12 files changed, 420 insertions(+), 188 deletions(-) create mode 100644 tests/Json/Decoder.Tests.fs diff --git a/src/Json/ARCtrl.Json.fsproj b/src/Json/ARCtrl.Json.fsproj index 1638812f..58f5dfde 100644 --- a/src/Json/ARCtrl.Json.fsproj +++ b/src/Json/ARCtrl.Json.fsproj @@ -31,13 +31,12 @@ - - + diff --git a/src/Json/Comment.fs b/src/Json/Comment.fs index bcb4373c..da11f95d 100644 --- a/src/Json/Comment.fs +++ b/src/Json/Comment.fs @@ -72,7 +72,7 @@ module CommentExtensions = static member toJsonString(?spaces) = fun (c:Comment) -> Comment.encoder c - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member fromROCrateJsonString (s:string) = Decode.fromJsonString Comment.ROCrate.decoder s @@ -81,12 +81,12 @@ module CommentExtensions = static member toROCrateJsonString(?spaces) = fun (c:Comment) -> Comment.ROCrate.encoder c - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member toISAJsonString(?spaces) = fun (c:Comment) -> Comment.ISAJson.encoder c - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member fromISAJsonString (s:string) = Decode.fromJsonString Comment.ISAJson.decoder s diff --git a/src/Json/Decode.fs b/src/Json/Decode.fs index 4d64f92a..6d981d87 100644 --- a/src/Json/Decode.fs +++ b/src/Json/Decode.fs @@ -85,6 +85,19 @@ module Decode = Error fst } + let noAdditionalProperties (allowedProperties : string seq) (decoder : Decoder<'value>) : Decoder<'value> = + let allowedProperties = Set.ofSeq allowedProperties + { new Decoder<'value> with + member _.Decode(helpers, value) = + let getters = Decode.Getters(helpers, value) + if hasUnknownFields helpers allowedProperties value then + Error (DecoderError("Unknown fields in object", ErrorReason.BadPrimitive("",value))) + else + decoder.Decode(helpers,value) + } + + + let resizeArray (decoder: Decoder<'value>) : Decoder> = { new Decoder> with member _.Decode(helpers, value) = diff --git a/src/Json/Encode.fs b/src/Json/Encode.fs index fc808f6f..3729b111 100644 --- a/src/Json/Encode.fs +++ b/src/Json/Encode.fs @@ -95,3 +95,8 @@ module Encode = name, if List.isEmpty value then Encode.nil else value |> List.map encoder |> Encode.list + + + let DefaultSpaces = 0 + + let defaultSpaces spaces = defaultArg spaces DefaultSpaces \ No newline at end of file diff --git a/src/Json/OntologyAnnotation.fs b/src/Json/OntologyAnnotation.fs index 6281912b..e32f848f 100644 --- a/src/Json/OntologyAnnotation.fs +++ b/src/Json/OntologyAnnotation.fs @@ -116,7 +116,7 @@ module OntologyAnnotationExtensions = static member toJsonString(?spaces) = fun (obj:OntologyAnnotation) -> OntologyAnnotation.encoder obj - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member fromROCrateJsonString (s:string) = Decode.fromJsonString OntologyAnnotation.ROCrate.decoder s @@ -125,12 +125,12 @@ module OntologyAnnotationExtensions = static member toROCrateJsonString(?spaces) = fun (obj:OntologyAnnotation) -> OntologyAnnotation.ROCrate.encoder obj - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member toISAJsonString(?spaces) = fun (obj:OntologyAnnotation) -> OntologyAnnotation.ISAJson.encoder obj - |> Encode.toJsonString (defaultArg spaces 2) + |> Encode.toJsonString (Encode.defaultSpaces spaces) static member fromISAJsonString (s:string) = Decode.fromJsonString OntologyAnnotation.ISAJson.decoder s \ No newline at end of file diff --git a/src/Json/OntologySourceReference.fs b/src/Json/OntologySourceReference.fs index 61a343ca..a1e8bb95 100644 --- a/src/Json/OntologySourceReference.fs +++ b/src/Json/OntologySourceReference.fs @@ -14,7 +14,6 @@ module OntologySourceReference = Encode.tryInclude "name" Encode.string (osr.Name) Encode.tryInclude "version" Encode.string (osr.Version) Encode.tryIncludeSeq "comments" Comment.encoder (osr.Comments) - "@context", ROCrateContext.OntologySourceReference.context_jsonvalue ] |> Encode.choose |> Encode.object @@ -55,7 +54,7 @@ module OntologySourceReference = |> Encode.choose |> Encode.object - let decoder (options : ConverterOptions) : Decoder = + let decoder : Decoder = Decode.object (fun get -> OntologySourceReference( ?description = get.Optional.Field "description" Decode.uri, @@ -66,31 +65,36 @@ module OntologySourceReference = ) ) -module OntologySourceReference = - - - let fromJsonString (s:string) = - Decode.fromJsonString decoder s + module ISAJson = + let encoder = encoder + let decoder = decoder + +[] +module OntologySourceReferenceExtensions = - let fromJsonldString (s:string) = - Decode.fromJsonString (decoder (ConverterOptions(IsJsonLD=true))) s + type OntologySourceReference with + + static member fromJsonString (s:string) = + Decode.fromJsonString OntologySourceReference.decoder s - let toJsonString (oa:OntologySourceReference) = - encoder (ConverterOptions()) oa - |> Encode.toJsonString 2 + static member toJsonString(?spaces) = + fun (obj:OntologySourceReference) -> + OntologySourceReference.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - /// exports in json-ld format - let toJsonldString (oa:OntologySourceReference) = - encoder (ConverterOptions(SetID=true,IsJsonLD=true)) oa - |> Encode.toJsonString 2 + static member fromROCrateJsonString (s:string) = + Decode.fromJsonString OntologySourceReference.ROCrate.decoder s - let toJsonldStringWithContext (a:OntologySourceReference) = - encoder (ConverterOptions(SetID=true,IsJsonLD=true)) a - |> Encode.toJsonString 2 + /// exports in json-ld format + static member toROCrateJsonString(?spaces) = + fun (obj:OntologySourceReference) -> + OntologySourceReference.ROCrate.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - // let fromFile (path : string) = - // File.ReadAllText path - // |> fromString + static member toISAJsonString(?spaces) = + fun (obj:OntologySourceReference) -> + OntologySourceReference.ISAJson.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - //let toFile (path : string) (osr:OntologySourceReference) = - // File.WriteAllText(path,toString osr) \ No newline at end of file + static member fromISAJsonString (s:string) = + Decode.fromJsonString OntologySourceReference.ISAJson.decoder s \ No newline at end of file diff --git a/src/Json/Person.fs b/src/Json/Person.fs index b0b75713..c8a6e7f5 100644 --- a/src/Json/Person.fs +++ b/src/Json/Person.fs @@ -4,123 +4,225 @@ open Thoth.Json.Core open ARCtrl open System.IO +open ARCtrl.Process.Conversion -module Person = - - let genID (p:Person) : string = - match p.ID with - | Some id -> URI.toString id - | None -> - let orcid = match p.Comments with - | Some cl -> cl |> Array.tryPick (fun c -> - match (c.Name,c.Value) with - | (Some n,Some v) -> if (n="orcid" || n="Orcid" || n="ORCID") then Some v else None - | _ -> None ) - | None -> None +module Person = + + let encoder (person : Person) = + [ + Encode.tryInclude "firstName" Encode.string person.FirstName + Encode.tryInclude "lastName" Encode.string person.LastName + Encode.tryInclude "midInitials" Encode.string person.MidInitials + Encode.tryInclude "orcid" Encode.string person.ORCID + Encode.tryInclude "email" Encode.string person.EMail + Encode.tryInclude "phone" Encode.string person.Phone + Encode.tryInclude "fax" Encode.string person.Fax + Encode.tryInclude "address" Encode.string person.Address + Encode.tryInclude "affiliation" Encode.string person.Affiliation + Encode.tryIncludeSeq "roles" OntologyAnnotation.encoder person.Roles + Encode.tryIncludeSeq "comments" Comment.encoder person.Comments + ] + |> Encode.choose + |> Encode.object + + let decoder : Decoder = + Decode.object (fun get -> + Person( + ?orcid=get.Optional.Field "orcid" Decode.string, + ?lastName=get.Optional.Field "lastName" Decode.string, + ?firstName=get.Optional.Field "firstName" Decode.string, + ?midInitials=get.Optional.Field "midInitials" Decode.string, + ?email=get.Optional.Field "email" Decode.string, + ?phone=get.Optional.Field "phone" Decode.string, + ?fax=get.Optional.Field "fax" Decode.string, + ?address=get.Optional.Field "address" Decode.string, + ?affiliation=get.Optional.Field "affiliation" Decode.string, + ?roles=get.Optional.Field "roles" (Decode.resizeArray OntologyAnnotation.decoder), + ?comments=get.Optional.Field "comments" (Decode.resizeArray Comment.decoder) + ) + ) + + module ROCrate = + + let genID (p:Person) : string = + let orcid = + p.Comments |> Seq.tryPick (fun c -> + match (c.Name,c.Value) with + | (Some n,Some v) -> if (n="orcid" || n="Orcid" || n="ORCID") then Some v else None + | _ -> None ) match orcid with | Some o -> o - | None -> match p.EMail with - | Some e -> e.ToString() - | None -> match (p.FirstName,p.MidInitials,p.LastName) with - | (Some fn,Some mn,Some ln) -> "#" + fn.Replace(" ","_") + "_" + mn.Replace(" ","_") + "_" + ln.Replace(" ","_") - | (Some fn,None,Some ln) -> "#" + fn.Replace(" ","_") + "_" + ln.Replace(" ","_") - | (None,None,Some ln) -> "#" + ln.Replace(" ","_") - | (Some fn,None,None) -> "#" + fn.Replace(" ","_") - | _ -> "#EmptyPerson" - - let affiliationEncoder (options : ConverterOptions) (affiliation : string) = - if options.IsJsonLD then - let idStr = $"#Organization_{affiliation}" - [ - ("@type",Encode.string "Organization") - ("@id",Encode.string (idStr.Replace(" ","_"))) - ("name",Encode.string affiliation) - if options.IsJsonLD then + | None -> + match p.EMail with + | Some e -> e.ToString() + | None -> + match (p.FirstName,p.MidInitials,p.LastName) with + | (Some fn,Some mn,Some ln) -> "#" + fn.Replace(" ","_") + "_" + mn.Replace(" ","_") + "_" + ln.Replace(" ","_") + | (Some fn,None,Some ln) -> "#" + fn.Replace(" ","_") + "_" + ln.Replace(" ","_") + | (None,None,Some ln) -> "#" + ln.Replace(" ","_") + | (Some fn,None,None) -> "#" + fn.Replace(" ","_") + | _ -> "#EmptyPerson" + + module Affiliation = + let encoder (affiliation : string) = + let idStr = $"#Organization_{affiliation}" + [ + ("@type",Encode.string "Organization") + ("@id",Encode.string (idStr.Replace(" ","_"))) + ("name",Encode.string affiliation) "@context", ROCrateContext.Organization.context_jsonvalue - ] - |> Encode.object - else - Encode.string affiliation - - let affiliationDecoder (options : ConverterOptions) : Decoder = - if options.IsJsonLD then - Decode.object (fun get -> - get.Required.Field "name" Decode.string - ) - else - Decode.string + ] + |> Encode.object + let decoder: Decoder = + Decode.object (fun get -> + get.Required.Field "name" Decode.string + ) - let encoder (options : ConverterOptions) (oa : Person) = - let oa = oa |> Person.setCommentFromORCID - let commentEncoder = if options.IsJsonLD then Comment.encoderDisambiguatingDescription else Comment.encoder options - [ - if options.SetID then + let encoder (oa : Person) = + [ "@id", Encode.string (oa |> genID) - else - Encode.tryInclude "@id" Encode.string (oa.ID) - if options.IsJsonLD then "@type", Encode.string "Person" - Encode.tryInclude "firstName" Encode.string (oa.FirstName) - Encode.tryInclude "lastName" Encode.string (oa.LastName) - Encode.tryInclude "midInitials" Encode.string (oa.MidInitials) - Encode.tryInclude "email" Encode.string (oa.EMail) - Encode.tryInclude "phone" Encode.string (oa.Phone) - Encode.tryInclude "fax" Encode.string (oa.Fax) - Encode.tryInclude "address" Encode.string (oa.Address) - Encode.tryInclude "affiliation" (affiliationEncoder options) (oa.Affiliation) - Encode.tryIncludeArray "roles" (OntologyAnnotation.encoder options) (oa.Roles) - Encode.tryIncludeArray "comments" commentEncoder (oa.Comments) - if options.IsJsonLD then + Encode.tryInclude "orcid" Encode.string oa.ORCID + Encode.tryInclude "firstName" Encode.string oa.FirstName + Encode.tryInclude "lastName" Encode.string oa.LastName + Encode.tryInclude "midInitials" Encode.string oa.MidInitials + Encode.tryInclude "email" Encode.string oa.EMail + Encode.tryInclude "phone" Encode.string oa.Phone + Encode.tryInclude "fax" Encode.string oa.Fax + Encode.tryInclude "address" Encode.string oa.Address + Encode.tryInclude "affiliation" Affiliation.encoder oa.Affiliation + Encode.tryIncludeSeq "roles" OntologyAnnotation.encoder oa.Roles + Encode.tryIncludeSeq "comments" Comment.ROCrate.encoderDisambiguatingDescription oa.Comments "@context", ROCrateContext.Person.context_jsonvalue - ] - |> Encode.choose - |> Encode.object + ] + |> Encode.choose + |> Encode.object - let allowedFields = ["@id";"firstName";"lastName";"midInitials";"email";"phone";"fax";"address";"affiliation";"roles";"comments";"@type"; "@context"] - - let decoder (options : ConverterOptions) : Decoder = - GDecode.object allowedFields (fun get -> - { - ID = get.Optional.Field "@id" GDecode.uri - ORCID = None - FirstName = get.Optional.Field "firstName" Decode.string - LastName = get.Optional.Field "lastName" Decode.string - MidInitials = get.Optional.Field "midInitials" Decode.string - EMail = get.Optional.Field "email" Decode.string - Phone = get.Optional.Field "phone" Decode.string - Fax = get.Optional.Field "fax" Decode.string - Address = get.Optional.Field "address" Decode.string - Affiliation = get.Optional.Field "affiliation" (affiliationDecoder options) - Roles = get.Optional.Field "roles" (Decode.array (OntologyAnnotation.decoder options)) - Comments = get.Optional.Field "comments" (Decode.array (Comment.decoder options)) - } - |> Person.setOrcidFromComments - - ) - let fromJsonString (s:string) = - GDecode.fromJsonString (decoder (ConverterOptions())) s + let decoder: Decoder = + Decode.object (fun get -> + Person( + ?orcid=get.Optional.Field "orcid" Decode.string, //is set afterwards with "Person.setOrcidFromComments" + ?lastName=get.Optional.Field "lastName" Decode.string, + ?firstName=get.Optional.Field "firstName" Decode.string, + ?midInitials=get.Optional.Field "midInitials" Decode.string, + ?email=get.Optional.Field "email" Decode.string, + ?phone=get.Optional.Field "phone" Decode.string, + ?fax=get.Optional.Field "fax" Decode.string, + ?address=get.Optional.Field "address" Decode.string, + ?affiliation=get.Optional.Field "affiliation" Affiliation.decoder, + ?roles=get.Optional.Field "roles" (Decode.resizeArray OntologyAnnotation.decoder), + ?comments=get.Optional.Field "comments" (Decode.resizeArray Comment.ROCrate.decoderDisambiguatingDescription) + ) + |> Person.setOrcidFromComments + ) + + /// + /// This is only used for ro-crate creation. In ISA publication authors are only a string. ro-crate requires person object. + /// Therefore, we try to split the string by common separators and create a minimal person object for ro-crate. + /// + /// + let encodeAuthorListString (authorList: string) = + let tab = "\t" + let semi = ";" + let comma = "," + let separator = + if authorList.Contains(tab) then tab + elif authorList.Contains(semi) then semi + else comma + let names = authorList.Split([|separator|], System.StringSplitOptions.None) + let encodeSingle (name:string) = + [ + "@type", Encode.string "Person" + Encode.tryInclude "name" Encode.string (Some name) + "@context", ROCrateContext.Person.contextMinimal_jsonValue + ] + |> Encode.choose + |> Encode.object + Encode.array (names |> Array.map encodeSingle) + + /// + /// This is only used for ro-crate creation. In ISA publication authors are only a string. ro-crate requires person object. + /// Therefore, we try to split the string by common separators and create a minimal person object for ro-crate. + /// + /// + let decodeAuthorListString: Decoder = + let decodeSingle: Decoder = + Decode.object (fun get -> + get.Optional.Field "name" Decode.string |> Option.defaultValue "" + ) + Decode.array decodeSingle + |> Decode.map (fun v -> + let cs = v |> String.concat ", " + cs + ) + + module ISAJson = + + let allowedFields = ["@id";"firstName";"lastName";"midInitials";"email";"phone";"fax";"address";"affiliation";"roles";"comments";"@type"; "@context"] + + let encoder (person : Person) = + let person = Person.setCommentFromORCID person + [ + Encode.tryInclude "firstName" Encode.string person.FirstName + Encode.tryInclude "lastName" Encode.string person.LastName + Encode.tryInclude "midInitials" Encode.string person.MidInitials + Encode.tryInclude "email" Encode.string person.EMail + Encode.tryInclude "phone" Encode.string person.Phone + Encode.tryInclude "fax" Encode.string person.Fax + Encode.tryInclude "address" Encode.string person.Address + Encode.tryInclude "affiliation" Encode.string person.Affiliation + Encode.tryIncludeSeq "roles" OntologyAnnotation.encoder person.Roles + Encode.tryIncludeSeq "comments" Comment.encoder person.Comments + ] + |> Encode.choose + |> Encode.object + + let decoder: Decoder = + Decode.objectNoAdditionalProperties allowedFields (fun get -> + Person( + ?orcid=None, //is set later by "Person.setOrcidFromComments" + ?lastName=get.Optional.Field "lastName" Decode.string, + ?firstName=get.Optional.Field "firstName" Decode.string, + ?midInitials=get.Optional.Field "midInitials" Decode.string, + ?email=get.Optional.Field "email" Decode.string, + ?phone=get.Optional.Field "phone" Decode.string, + ?fax=get.Optional.Field "fax" Decode.string, + ?address=get.Optional.Field "address" Decode.string, + ?affiliation=get.Optional.Field "affiliation" Decode.string, + ?roles=get.Optional.Field "roles" (Decode.resizeArray OntologyAnnotation.decoder), + ?comments=get.Optional.Field "comments" (Decode.resizeArray Comment.decoder) + ) + |> Person.setOrcidFromComments + ) + +[] +module PersonExtensions = - let fromJsonldString (s:string) = - GDecode.fromJsonString (decoder (ConverterOptions(IsJsonLD=true))) s + type Person with + + static member fromJsonString (s:string) = + Decode.fromJsonString Person.decoder s - let toJsonString (p:Person) = - encoder (ConverterOptions()) p - |> Encode.toJsonString 2 + static member toJsonString(?spaces) = + fun (obj:Person) -> + Person.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - /// exports in json-ld format - let toJsonldString (p:Person) = - encoder (ConverterOptions(SetID=true,IsJsonLD=true)) p - |> Encode.toJsonString 2 + static member fromROCrateJsonString (s:string) = + Decode.fromJsonString Person.ROCrate.decoder s - let toJsonldStringWithContext (a:Person) = - encoder (ConverterOptions(SetID=true,IsJsonLD=true)) a - |> Encode.toJsonString 2 + /// exports in json-ld format + static member toROCrateJsonString(?spaces) = + fun (obj:Person) -> + Person.ROCrate.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - //let fromFile (path : string) = - // File.ReadAllText path - // |> fromString + static member toISAJsonString(?spaces) = + fun (obj:Person) -> + Person.ISAJson.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - //let toFile (path : string) (p:Person) = - // File.WriteAllText(path,toString p) \ No newline at end of file + static member fromISAJsonString (s:string) = + Decode.fromJsonString Person.ISAJson.decoder s \ No newline at end of file diff --git a/src/Json/Publication.fs b/src/Json/Publication.fs index 345bdfa3..9f554cc6 100644 --- a/src/Json/Publication.fs +++ b/src/Json/Publication.fs @@ -5,69 +5,118 @@ open Thoth.Json.Core open ARCtrl open System.IO -module Publication = - - let genID (p:Publication) = - match p.DOI with - | Some doi -> doi - | None -> match p.PubMedID with - | Some id -> id - | None -> match p.Title with - | Some t -> "#Pub_" + t.Replace(" ","_") - | None -> "#EmptyPublication" - - let encoder (options : ConverterOptions) (oa : Publication) = - let commentEncoder = if options.IsJsonLD then Comment.encoderDisambiguatingDescription else Comment.encoder options - let authorListEncoder = if options.IsJsonLD then ROCrateHelper.Person.authorListStrinEncoder else Encode.string +module Publication = + + let encoder (oa : Publication) = [ - if options.SetID then - "@id", Encode.string (oa |> genID) - if options.IsJsonLD then - "@type", Encode.string "Publication" Encode.tryInclude "pubMedID" Encode.string (oa.PubMedID) Encode.tryInclude "doi" Encode.string (oa.DOI) - Encode.tryInclude "authorList" authorListEncoder (oa.Authors) + Encode.tryInclude "authorList" Encode.string (oa.Authors) Encode.tryInclude "title" Encode.string (oa.Title) - Encode.tryInclude "status" (OntologyAnnotation.encoder options) (oa.Status) - Encode.tryIncludeArray "comments" commentEncoder (oa.Comments) - if options.IsJsonLD then - "@context", ROCrateContext.Publication.context_jsonvalue + Encode.tryInclude "status" OntologyAnnotation.encoder (oa.Status) + Encode.tryIncludeSeq "comments" Comment.encoder oa.Comments ] |> Encode.choose |> Encode.object - let allowedFields = ["@id";"pubMedID";"doi";"authorList";"title";"status";"comments";"@type"; "@context"] - - let decoder (options : ConverterOptions) : Decoder = - GDecode.object allowedFields (fun get -> - { - PubMedID = get.Optional.Field "pubMedID" GDecode.uri - DOI = get.Optional.Field "doi" Decode.string - Authors = get.Optional.Field "authorList" Decode.string - Title = get.Optional.Field "title" Decode.string - Status = get.Optional.Field "status" (OntologyAnnotation.decoder options) - Comments = get.Optional.Field "comments" (Decode.array (Comment.decoder options)) - } - + let decoder : Decoder = + Decode.object (fun get -> + Publication( + ?pubMedID = get.Optional.Field "pubMedID" Decode.uri, + ?doi= get.Optional.Field "doi" Decode.string, + ?authors = get.Optional.Field "authorList" Decode.string, + ?title = get.Optional.Field "title" Decode.string, + ?status = get.Optional.Field "status" OntologyAnnotation.decoder, + ?comments = get.Optional.Field "comments" (Decode.resizeArray Comment.decoder) + ) ) - let fromJsonString (s:string) = - GDecode.fromJsonString (decoder (ConverterOptions())) s - let fromJsonldString (s:string) = - GDecode.fromJsonString (decoder (ConverterOptions(IsJsonLD=true))) s + module ROCrate = + + let genID (p:Publication) = + match p.DOI with + | Some doi -> doi + | None -> + match p.PubMedID with + | Some id -> id + | None -> + match p.Title with + | Some t -> "#Pub_" + t.Replace(" ","_") + | None -> "#EmptyPublication" + + let encoder (oa : Publication) = + [ + "@id", Encode.string (oa |> genID) + "@type", Encode.string "Publication" + Encode.tryInclude "pubMedID" Encode.string oa.PubMedID + Encode.tryInclude "doi" Encode.string (oa.DOI) + Encode.tryInclude "authorList" Person.ROCrate.encodeAuthorListString oa.Authors + Encode.tryInclude "title" Encode.string (oa.Title) + Encode.tryInclude "status" OntologyAnnotation.encoder oa.Status + Encode.tryIncludeSeq "comments" Comment.ROCrate.encoderDisambiguatingDescription oa.Comments + "@context", ROCrateContext.Publication.context_jsonvalue + ] + |> Encode.choose + |> Encode.object + + let decoder : Decoder = + Decode.object (fun get -> + Publication( + ?pubMedID = get.Optional.Field "pubMedID" Decode.uri, + ?doi= get.Optional.Field "doi" Decode.string, + ?authors = get.Optional.Field "authorList" Person.ROCrate.decodeAuthorListString, + ?title = get.Optional.Field "title" Decode.string, + ?status = get.Optional.Field "status" OntologyAnnotation.decoder, + ?comments = get.Optional.Field "comments" (Decode.resizeArray Comment.ROCrate.decoderDisambiguatingDescription) + ) + ) + + module ISAJson = + + let encoder (oa : Publication) = + [ + Encode.tryInclude "pubMedID" Encode.string oa.PubMedID + Encode.tryInclude "doi" Encode.string (oa.DOI) + Encode.tryInclude "authorList" Encode.string oa.Authors + Encode.tryInclude "title" Encode.string (oa.Title) + Encode.tryInclude "status" OntologyAnnotation.encoder oa.Status + Encode.tryIncludeSeq "comments" Comment.ROCrate.encoderDisambiguatingDescription oa.Comments + ] + |> Encode.choose + |> Encode.object + + let allowedFields = ["pubMedID";"doi";"authorList";"title";"status";"comments";] + + let decoder : Decoder = + decoder + |> Decode.noAdditionalProperties allowedFields + +[] +module PublicationExtensions = + + type Publication with + + static member fromJsonString (s:string) = + Decode.fromJsonString Publication.decoder s + + static member toJsonString(?spaces) = + fun (obj:Publication) -> + Publication.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - let toJsonString (p:Publication) = - encoder (ConverterOptions()) p - |> Encode.toJsonString 2 + static member fromROCrateJsonString (s:string) = + Decode.fromJsonString Publication.ROCrate.decoder s - /// exports in json-ld format - let toJsonldString (p:Publication) = - encoder (ConverterOptions(SetID=true,IsJsonLD=true)) p - |> Encode.toJsonString 2 + /// exports in json-ld format + static member toROCrateJsonString(?spaces) = + fun (obj:Publication) -> + Publication.ROCrate.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - //let fromFile (path : string) = - // File.ReadAllText path - // |> fromString + static member toISAJsonString(?spaces) = + fun (obj:Publication) -> + Publication.ISAJson.encoder obj + |> Encode.toJsonString (Encode.defaultSpaces spaces) - //let toFile (path : string) (p:Publication) = - // File.WriteAllText(path,toString p) \ No newline at end of file + static member fromISAJsonString (s:string) = + Decode.fromJsonString Publication.ISAJson.decoder s \ No newline at end of file diff --git a/src/Json/context/rocrate/isa_person_context.fs b/src/Json/context/rocrate/isa_person_context.fs index 392bde9b..ed778328 100644 --- a/src/Json/context/rocrate/isa_person_context.fs +++ b/src/Json/context/rocrate/isa_person_context.fs @@ -27,7 +27,7 @@ module Person = "sdo", Encode.string "http://schema.org/" "Person", Encode.string "sdo:Person" - + "orcid", Encode.string "sdo:identifier" "firstName", Encode.string "sdo:givenName" "lastName", Encode.string "sdo:familyName" "midInitials", Encode.string "sdo:additionalName" diff --git a/tests/Json/ARCtrl.Json.Tests.fsproj b/tests/Json/ARCtrl.Json.Tests.fsproj index 0a85f313..9978409c 100644 --- a/tests/Json/ARCtrl.Json.Tests.fsproj +++ b/tests/Json/ARCtrl.Json.Tests.fsproj @@ -6,6 +6,7 @@ false + diff --git a/tests/Json/Decoder.Tests.fs b/tests/Json/Decoder.Tests.fs new file mode 100644 index 00000000..a11b74c1 --- /dev/null +++ b/tests/Json/Decoder.Tests.fs @@ -0,0 +1,58 @@ +module Tests.Decoder + +open TestingUtils +open ARCtrl +open ARCtrl.Json + +module private TestHelper = + + open Thoth.Json.Core + + type Person = { + Name: string + Age: int + Gender: string + } + + let encoder (person: Person) = + Encode.object [ + "name", Encode.string person.Name + "age", Encode.int person.Age + "gender", Encode.string person.Gender + ] + + let decoder : Decoder = + Decode.object (fun get -> + { + Name = get.Optional.Field "name" Decode.string |> Option.defaultValue "" + Age = get.Optional.Field "age" Decode.int |> Option.defaultValue 0 + Gender = get.Optional.Field "gender" Decode.string |> Option.defaultValue "" + } + ) + +open TestHelper + +let private tests_NoAdditionalProperties = testList "NoAdditionalProperties" [ + testCase "ensure" <| fun _ -> + let person = {Name="Kevin"; Age = 30; Gender = "Male"} + let jsonStr = encoder person |> Encode.toJsonString 0 + let person2 = Decode.fromJsonString decoder jsonStr + Expect.equal person2 person "" + testCase "fail" <| fun _ -> + let person = {Name="Kevin"; Age = 30; Gender = "Male"} + let decoder = decoder |> Decode.noAdditionalProperties ["name"; "age"] + let jsonStr = encoder person |> Encode.toJsonString 0 + let person2 = fun () -> Decode.fromJsonString decoder jsonStr |> ignore + Expect.throws person2 "" + testCase "success" <| fun _ -> + let person = {Name="Kevin"; Age = 30; Gender = "Male"} + let decoder = decoder |> Decode.noAdditionalProperties ["name"; "age"; "gender"] + let jsonStr = encoder person |> Encode.toJsonString 0 + let person2 = Decode.fromJsonString decoder jsonStr + Expect.equal person2 person "" +] + +let Main = testList "Decoder" [ + tests_NoAdditionalProperties +] + diff --git a/tests/Json/Main.fs b/tests/Json/Main.fs index 632010c0..b5febee2 100644 --- a/tests/Json/Main.fs +++ b/tests/Json/Main.fs @@ -4,6 +4,7 @@ open Fable.Pyxpecto let all = testSequenced <| testList "ISA.JSON" [ + Tests.Decoder.Main Json.Tests.main JsonSchema.Tests.main Tests.ROCrate.Main