diff --git a/src/GslCore/AlleleSwaps.fs b/src/GslCore/AlleleSwaps.fs index 1002ebe..0fd09d2 100644 --- a/src/GslCore/AlleleSwaps.fs +++ b/src/GslCore/AlleleSwaps.fs @@ -4,6 +4,7 @@ /// Support for introducing mutations into genes /// open Amyris.Bio +open Amyris.ErrorHandling open Amyris.Dna open System open constants @@ -17,7 +18,7 @@ open IO.CodonUsage open utils open biolib open primercore -open ryse // for getrabit +open FetchPart open PluginTypes @@ -121,16 +122,9 @@ let selectMutCodonRight = selectMutCodonBase diffRight /// expand a simple mutation inline with a part let expandSimpleMut (asAACheck:bool) (_:GenomeDef) (g:PartIdLegacy) (m:Mutation) : GslSourceCode = - // Get part sequence - if not (g.id.StartsWith("R")) then - failwithf - "ERROR: part %s should start with 'R'. Non rabit part mutation not supported." - g.id - - let hr = getRabit (int(g.id.[1..])) - - let rabit = hr.RabitSpecs.[0] - let dna = rabit.DnaElementSpecs.[0].DnaSequence.ToUpper() + let dna = + let part = fetchPart g.id |> returnOrFail + part.dna // Now split by type of mutation and check the original base/amino acid is legit match m.mType with | NT -> @@ -158,7 +152,7 @@ let expandSimpleMut (asAACheck:bool) (_:GenomeDef) (g:PartIdLegacy) (m:Mutation) "ERROR: mutation position %d outside range of rabit %s amino acids" m.loc g.id - let currentCodon = (dna.[(m.loc-1)*3..(m.loc-1)*3+2]).ToCharArray() + let currentCodon = (dna.[(m.loc-1)*3..(m.loc-1)*3+2]).arr // Ensure we are in the right place in the gene if (codon2aa currentCodon <> m.f) && asAACheck then diff --git a/src/GslCore/AstExpansion.fs b/src/GslCore/AstExpansion.fs index f2f5769..5aae344 100644 --- a/src/GslCore/AstExpansion.fs +++ b/src/GslCore/AstExpansion.fs @@ -22,6 +22,7 @@ open commonTypes open resolveExtPart open LexAndParse open PluginTypes +open FetchPart // ================== // phase 1 of AST reduction @@ -879,12 +880,12 @@ let private expandHB // External part variation // =============================================== - match fetchFullPartSequence(verbose) (Map.empty) pid1 with - | EXT_FAIL(msg) -> failwithf "Fail fetching %s %s" pid1.id msg - | EXT_FETCH_OK(part1) -> - match fetchFullPartSequence verbose Map.empty pid4 with - | EXT_FAIL(msg) -> failwithf "Fail fetching %s %s" pid1.id msg - | EXT_FETCH_OK(part4) -> + match fetchPart pid1.id with + | Bad msgs -> failwithf "Fail fetching %s %s" pid1.id (msgs |> String.concat " ") + | Ok(part1, _) -> + match fetchPart pid4.id with + | Bad msgs -> failwithf "Fail fetching %s %s" pid1.id (msgs |> String.concat " ") + | Ok(part4, _) -> let s1= getExtPartSlice verbose pid1 let s4= getExtPartSlice verbose pid4 diff --git a/src/GslCore/DnaCreation.fs b/src/GslCore/DnaCreation.fs index f56acd8..2dccd6d 100644 --- a/src/GslCore/DnaCreation.fs +++ b/src/GslCore/DnaCreation.fs @@ -578,7 +578,7 @@ let expandAssembly yield markerProvider.CreateDna(task) | PARTID(partId) -> - yield resolveExtPart.fetchSequence verbose library ppp partId + yield resolveExtPart.fetchSequence verbose ppp partId | INLINEDNA(dna) -> yield expandInlineDna dnaSource ppp dna | INLINEPROT(_) -> diff --git a/src/GslCore/FetchPart.fs b/src/GslCore/FetchPart.fs new file mode 100644 index 0000000..ca0e2ed --- /dev/null +++ b/src/GslCore/FetchPart.fs @@ -0,0 +1,46 @@ +/// Plugin-based retrieval of parts from external catalogs. +module FetchPart + +open PluginTypes +open Amyris.ErrorHandling +open Amyris.Dna + +// In an ideal world, this collection of plugins would be injected from the top, but at the time of this writing the +// compiler uses part retrieval functions in a large number of places, requiring a frustrating quantity of refactoring. +// Thus, this module-bound global structure. + +let mutable private partProviders: IPartProvider array = Array.empty + +/// Set the available part providers. +/// This should be called once at program initialization. +let setPartProviders providers = partProviders <- providers + +type ExternalPart = { + /// Persistent identifier. + id: string + /// Human-readable name. + name: string + /// DNA sequence of this part. + dna: Dna + /// Optional RYSE linker specification (5' link code, 3' link code). + linkers: (string*string) option + /// Name of the part provider that provided this part. + source: string +} + +/// Use the available part providers to try to fetch a part based on its ID. +let fetchPart partId : Result = + match partProviders |> Array.filter (fun p -> p.Accept(partId)) with + | [||] -> fail <| sprintf "No external part provider found for part ID \"%s\"." partId + | [|provider|] -> + provider.Retrieve(partId) + >>= (fun fetched -> + ok { + id = partId + source = provider.Name + name = fetched.name + linkers = fetched.linkers + dna = fetched.dna}) + | tooMany -> + let names = tooMany |> Array.map (fun p -> p.Name) |> String.concat ", " + fail <| sprintf "More than one part provider service found for part ID \"%s\": %s" partId names \ No newline at end of file diff --git a/src/GslCore/GslCore.fsproj b/src/GslCore/GslCore.fsproj index ea81597..933546c 100644 --- a/src/GslCore/GslCore.fsproj +++ b/src/GslCore/GslCore.fsproj @@ -1,5 +1,4 @@ - - + netstandard2.0 GslCore @@ -9,12 +8,8 @@ - - --module GslParser -o GslParser.fs - - - --unicode -o GslLexer.fs - + + @@ -39,6 +34,7 @@ + @@ -60,7 +56,13 @@ - + + --unicode -o GslLexer.fs + + + --module GslParser -o GslParser.fs + + \ No newline at end of file diff --git a/src/GslCore/LibraryPartProvider.fs b/src/GslCore/LibraryPartProvider.fs new file mode 100644 index 0000000..a134175 --- /dev/null +++ b/src/GslCore/LibraryPartProvider.fs @@ -0,0 +1,30 @@ +/// Support for reusable part retrieval based on a FASTA file. +module LibraryPartProvider +open System.IO +open commonTypes +open PluginTypes +open Amyris.Dna +open Amyris.ErrorHandling + +type LibraryPartProvider() = + let mutable library: SequenceLibrary = Map.empty + do + () + with + interface IPartProvider with + member __.ProvidedArgs() = [] + member x.Configure(_) = x :> IPartProvider + /// Load the sequence library from the GSLC lib directory, if it exists. + member x.ConfigureFromOptions(opts) = + let libFile = Path.Combine(opts.libDir, "lib.fa") + if File.Exists libFile then + let lib = + Amyris.Bio.biolib.readReference libFile + |> Seq.map (fun kv -> (kv.Key.ToUpper(), Dna(kv.Value) )) + |> Map.ofSeq + library <- lib + + x :> IPartProvider + member x.Name = "library" + member x.Accept(partId) = library |> Map.containsKey partId + member x.Retrieve(partId) = ok {name = partId; dna = library.[partId]; linkers = None} diff --git a/src/GslCore/PluginTypes.fs b/src/GslCore/PluginTypes.fs index 057d5aa..2791cf3 100644 --- a/src/GslCore/PluginTypes.fs +++ b/src/GslCore/PluginTypes.fs @@ -126,6 +126,30 @@ type L2Provider = { implicitLocusProvider:L2DesignParams-> GslSourceCode } +// ================================================== +// plugin for retrieving an existing part +// ================================================== + +type ExtFetchSeq = { + /// Human-readable name. + name: string + /// DNA sequence of this part. + dna: Dna + /// Optional RYSE linker specification (5' link code, 3' link code). + linkers: (string*string) option +} + +type IPartProvider = + /// Allow part providers to add command line args and be configurable. + inherit IConfigurable + /// The name of this part provider service. + abstract member Name: string + /// Return true if this provider thinks it recognizes the provided identifier. + abstract member Accept: string -> bool + /// Call the service to retrieve this part. + /// Implementors may assume that this method will only be called if Accept has returned true. + abstract member Retrieve: string -> Result + // ====================== // plugin behavior definition for output assembly transformations // ====================== @@ -208,6 +232,7 @@ type PluginBehavior = | L2KOTitration of L2Provider | OutputFormat of IOutputFormat | AssemblyTransform of IAssemblyTransform + | PartProvider of IPartProvider | CodonProvider of ICodonProvider | MarkerProvider of IMarkerProvider with @@ -215,6 +240,7 @@ type PluginBehavior = match b with | OutputFormat(f) -> f.ProvidedArgs() | AssemblyTransform(a) -> a.ProvidedArgs() + | PartProvider(p) -> p.ProvidedArgs() | CodonProvider(c) -> c.ProvidedArgs() | _ -> [] @@ -236,6 +262,7 @@ let configureBehavior arg b = match b.behavior with | OutputFormat(f) -> {b with behavior = OutputFormat(f.Configure(arg))} | AssemblyTransform(a) -> {b with behavior = AssemblyTransform(a.Configure(arg))} + | PartProvider(p) -> {b with behavior = PartProvider(p.Configure(arg))} | CodonProvider(c) -> {b with behavior = CodonProvider(c.Configure(arg))} | MarkerProvider(m) -> {b with behavior = MarkerProvider(m.Configure(arg))} | AlleleSwapAA _ @@ -245,6 +272,7 @@ let configureBehaviorFromOpts opts b = match b.behavior with | OutputFormat(f) -> {b with behavior = OutputFormat(f.ConfigureFromOptions(opts))} | AssemblyTransform(a) -> {b with behavior = AssemblyTransform(a.ConfigureFromOptions(opts))} + | PartProvider(p) -> {b with behavior = PartProvider(p.ConfigureFromOptions(opts))} | CodonProvider(c) -> {b with behavior = CodonProvider(c.ConfigureFromOptions(opts))} | MarkerProvider(c) -> {b with behavior = MarkerProvider(c.ConfigureFromOptions(opts))} | AlleleSwapAA _ @@ -330,6 +358,10 @@ let getAssemblyTransformers (plugin: Plugin) = plugin.behaviors |> List.choose (fun b -> match b.behavior with | AssemblyTransform(a) -> Some(a.TransformAssembly) | _ -> None) +let getPartProviders (plugin: Plugin) = + plugin.behaviors + |> List.choose (fun b -> match b.behavior with | PartProvider(a) -> Some(a) | _ -> None) + let getOutputProviders (plugin: Plugin) = plugin.behaviors |> List.choose (fun b -> match b.behavior with | OutputFormat(a) -> Some(a) | _ -> None) diff --git a/src/GslCore/ResolveExtPart.fs b/src/GslCore/ResolveExtPart.fs index 565faae..a477f11 100644 --- a/src/GslCore/ResolveExtPart.fs +++ b/src/GslCore/ResolveExtPart.fs @@ -2,208 +2,122 @@ open commonTypes open pragmaTypes open LegacyParseTypes -open ryse +open FetchPart open applySlices open Amyris.Bio.biolib open constants open Amyris.Dna open Amyris.ErrorHandling +open PluginTypes -type ExtFetchSeq = { id : string ; dna : Dna ; source : string ; name : string} -type ExtFetchResult = | EXT_FETCH_OK of ExtFetchSeq | EXT_FAIL of string - -let legalPrefixes = [ ("r","rabit") ; ("b","biobrick") ] - -/// Does this part id start with a legal external part prefix -let legalPartPrefix (pid:string) = - let pidLower = pid.ToLower() - let rec checkPrefix (prefs:(string*string) list) = - match prefs with - | [] -> None - | (tag,name)::_ when pidLower.StartsWith(tag) -> Some(name,pid.[tag.Length..]) - | _::tl -> checkPrefix tl - - checkPrefix legalPrefixes - -let fetchSequence (verbose:bool) (library: SequenceLibrary) (ppp:PPP) (partId:PartIdLegacy) = -// Sequence can come either from the libary or preferably from the hutch directly +let fetchSequence (verbose:bool) (ppp:PPP) (partId:PartIdLegacy) = let pid = partId.id let sliceName = match ppp.pr.TryGetOne("name") with | Some(name) -> name | None -> "" let uri = ppp.pr.TryGetOne("uri") - match legalPartPrefix pid with - | None -> - failwithf - "ERROR: partId reference %s isn't a defined alias and doesn't start with r for rabit\n" - pid - | Some(partSpace, _) -> - match partSpace with - | "rabit" -> - let libName = "@"+pid.ToUpper() - if not (library.ContainsKey(libName)) then - let hr = getRabit (int(pid.[1..])) - - // Have part from the hutch. We might just use it verbatim or we might be - // some modifications to it to make a new part - let rabit = hr.RabitSpecs.[0] - let dna = Dna(rabit.DnaElementSpecs.[0].DnaSequence) - // Check for slice modifications. We can't handle any other type of mod at this point, so - // ensure there are none. - if partId.mods |> List.exists (fun m -> match m with | SLICE(_)-> false | _ -> true) then - failwithf "ERROR: could not process mods for rabit %s %A\n" partId.id partId.mods + let part = fetchPart pid |> returnOrFail - // Look for simple case. If we are just using the part from the hutch unadulterated, then - // we specify things differently, referring to the external id - if partId.mods.Length = 0 then - let dna = if ppp.fwd then dna else dna.RevComp() - {id = None; - extId = Some(pid.[1..]); - sliceName = sliceName; - uri = uri; // TODO: use the URI of rabit from hutch here instead? - dna = dna; - sourceChr = "library"; - sourceFr = 0; - sourceTo = (dna.Length-1)*1; - sourceFwd = ppp.fwd; - sourceFrApprox = false; - sourceToApprox = false; - // Don't assign coordinates to pieces until later when we decide - // how they are getting joined up - destFr = 0; - destTo = 0; - destFwd = ppp.fwd; - description = rabit.Name; - sliceType = REGULAR; - amplified = false; - template = Some dna; // not amplifying from this - dnaSource = - match ppp.pr.TryGetOne("dnasrc") with - | Some(d) -> d - | None -> pid; - pragmas = ppp.pr; - breed = B_X; // will be replaced at final submission - materializedFrom = Some(ppp); - annotations = []} // FIXME: need to generate annotations based on Rabit metadata - else - // Otherwise, they are taking a hutch part and doing something to it, - // so the hutch is just another DNA source and they are effectively - // building a new rabit + // Have part from the hutch. We might just use it verbatim or we might be + // some modifications to it to make a new part + let part = fetchPart pid |> returnOrFail - // Start off assuming it's the full DNA slice - let startSlice = - {left = {x = 1; relTo = FivePrime}; - lApprox = false; - rApprox = false; - right = {x = -1; relTo = ThreePrime}} + // Check for slice modifications. We can't handle any other type of mod at this point, so + // ensure there are none. + if partId.mods |> List.exists (fun m -> match m with | SLICE(_)-> false | _ -> true) then + failwithf "ERROR: could not process mods for rabit %s %A\n" partId.id partId.mods - // Apply the slice(s) to get a final coordinate range - let finalSlice = applySlices verbose partId.mods startSlice - - // Find the left and right hand ends of the slice - let x, y = - getBoundsFromSlice finalSlice dna.Length (Library(partId.id)) - |> returnOrFail - - let finalDNA = - dna.[(x/1)-1..(y/1)-1] - |> DnaOps.revCompIf (not ppp.fwd) + // Look for simple case. If we are just using the part unadulterated, then + // we specify things differently, referring to the external id. + if partId.mods.Length = 0 then + let dna = part.dna |> DnaOps.revCompIf (not ppp.fwd) - let name1 = - if partId.mods.Length = 0 then rabit.Name - else (rabit.Name + (printSlice finalSlice)) - let name2 = if ppp.fwd then name1 else "!" + name1 + {id = None; + extId = Some pid; + sliceName = sliceName; + uri = uri; // TODO: use the URI of part here instead? + dna = dna; + sourceChr = "library"; + sourceFr = 0; + sourceTo = (dna.Length-1)*1; + sourceFwd = ppp.fwd; + sourceFrApprox = false; + sourceToApprox = false; + // Don't assign coordinates to pieces until later when we decide + // how they are getting joined up + destFr = 0; + destTo = 0; + destFwd = ppp.fwd; + description = part.name; + sliceType = REGULAR; + amplified = false; + template = Some dna; // not amplifying from this + dnaSource = + match ppp.pr.TryGetOne("dnasrc") with + | Some(d) -> d + | None -> pid; + pragmas = ppp.pr; + breed = B_X; // will be replaced at final submission + materializedFrom = Some(ppp); + annotations = []} // FIXME: need to generate annotations based on Rabit metadata + else + // Otherwise, they are taking a hutch part and doing something to it, + // so the hutch is just another DNA source and they are effectively + // building a new rabit - {id = None; - extId = None; - sliceName = sliceName; - uri = uri; // TODO: use URI from hutch part? mint new URI? - dna = finalDNA; - amplified = false; - template = Some finalDNA; - sourceChr = "library"; - sourceFr = (finalSlice.left.x/(1)-1)*1; - sourceTo = (finalSlice.right.x/(1)-1)*1; - sourceFwd = true; - sourceFrApprox = false; - sourceToApprox = false; - // Don't assign coordinates to pieces until later when we decide how they are getting joined up - destFr = 0; - destTo = 0; - destFwd = ppp.fwd; - description = name2; - sliceType = REGULAR; - dnaSource = - match ppp.pr.TryGetOne("dnasrc") with - | Some(d) -> d - | None -> pid; - pragmas = ppp.pr; - breed = B_X; // they are hacking rabit, all bets are off - materializedFrom = Some(ppp); - annotations = []} // FIXME: need to generate annotations based on Rabit metadata + // Start off assuming it's the full DNA slice + let startSlice = + {left = {x = 1; relTo = FivePrime}; + lApprox = false; + rApprox = false; + right = {x = -1; relTo = ThreePrime}} - else - // Part is in the library - let dna = library.[libName] - {id = None; - extId = Some(pid.[1..]); - sliceName = sliceName; - uri = uri; // TODO: mint new URI if None? - dna = dna; - template= Some dna; - amplified = false; - sourceChr = "library"; - sourceFr = 0; - sourceTo = (dna.Length-1)*1 - sourceFwd = true; - sourceFrApprox = false; - sourceToApprox = false; - // Don't assign coordinates to pieces until later when we decide - // how they are getting joined up - destFr = 0; - destTo = 0; - destFwd = ppp.fwd; - description = libName; - sliceType = REGULAR; - dnaSource = "library"; - pragmas = ppp.pr; - breed = B_X; - materializedFrom = Some(ppp); - annotations = []} // FIXME: determine what metadata is available here + // Apply the slice(s) to get a final coordinate range + let finalSlice = applySlices verbose partId.mods startSlice - | x -> - failwithf "ERROR: unimplemented external partSpace %s\n" x + // Find the left and right hand ends of the slice + let x, y = + getBoundsFromSlice finalSlice part.dna.Length (Library(partId.id)) + |> returnOrFail + let finalDNA = + part.dna.[(x/1)-1..(y/1)-1] + |> DnaOps.revCompIf (not ppp.fwd) -/// Get the full part sequence for this external reference, don't apply any slice mods to it -let fetchFullPartSequence (_ (* verbose*):bool) (library: SequenceLibrary) (partId:PartIdLegacy) = -// Sequence can come either from the libary or preferably from the hutch directly - let pid = partId.id - match legalPartPrefix pid with - | None -> EXT_FAIL( sprintf "ERROR: partId reference %s isn't a defined alias and doesn't start with r for rabit\n" pid) - | Some(partSpace, _) -> - match partSpace with - | "rabit" -> - let libName = "@"+pid.ToUpper() - if not (library.ContainsKey(libName)) then - let hr = getRabit (int(pid.[1..])) + let name1 = + if partId.mods.Length = 0 then part.name + else (part.name + (printSlice finalSlice)) + let name2 = if ppp.fwd then name1 else "!" + name1 - // Have part from the hutch. We might just use it verbatim or we might be - // some modifications to it to make a new part - let rabit = hr.RabitSpecs.[0] - let dna = Dna(rabit.DnaElementSpecs.[0].DnaSequence) - EXT_FETCH_OK({dna = dna; source = "hutch"; id = pid; name = rabit.Name}) - else - // Part is in the library - EXT_FETCH_OK( - {dna = library.[libName]; - source = "library"; - id = pid; - name = libName}) - | x -> - failwithf "ERROR: unimplemented external partSpace %s\n" x + {id = None; + extId = None; + sliceName = sliceName; + uri = uri; // TODO: use URI from hutch part? mint new URI? + dna = finalDNA; + amplified = false; + template = Some finalDNA; + sourceChr = "library"; + sourceFr = (finalSlice.left.x/(1)-1)*1; + sourceTo = (finalSlice.right.x/(1)-1)*1; + sourceFwd = true; + sourceFrApprox = false; + sourceToApprox = false; + // Don't assign coordinates to pieces until later when we decide how they are getting joined up + destFr = 0; + destTo = 0; + destFwd = ppp.fwd; + description = name2; + sliceType = REGULAR; + dnaSource = + match ppp.pr.TryGetOne("dnasrc") with + | Some(d) -> d + | None -> pid; + pragmas = ppp.pr; + breed = B_X; + materializedFrom = Some(ppp); + annotations = []} // FIXME: need to generate annotations based on Rabit metadata let getExtPartSlice (verbose:bool) (partId:PartIdLegacy) = // Start off assuming it's the full DNA slice @@ -219,7 +133,7 @@ let getExtPartSlice (verbose:bool) (partId:PartIdLegacy) = let applySliceToExtSequence (_ (* verbose*):bool) - (extPart:ExtFetchSeq) + (extPart:ExternalPart) (pr:PragmaCollection) (fwd:bool) (partId:PartIdLegacy) @@ -231,7 +145,7 @@ let applySliceToExtSequence let dna = extPart.dna |> DnaOps.revCompIf (not fwd) {id = None; - extId = Some(extPart.id.[1..]); + extId = Some extPart.id; sliceName = sliceName; uri = uri; // TODO: mint new URI if None? dna = dna; diff --git a/src/GslCore/Ryse.fs b/src/GslCore/Ryse.fs index 7644f5a..a2f35ea 100644 --- a/src/GslCore/Ryse.fs +++ b/src/GslCore/Ryse.fs @@ -1,17 +1,21 @@ module ryse open System.IO +open System.Text.RegularExpressions open FSharp.Data open System open pragmaTypes open commonTypes open Amyris.Bio.utils open Amyris.Dna +open Amyris.ErrorHandling //open System.Collections.Generic open System.Collections.Concurrent open constants open uri open sbolExample open AstTypes +open PluginTypes +open FetchPart // ================================================================== // RYSE megastitch architecture @@ -19,14 +23,6 @@ open AstTypes /// RYSE verbose flag let private verbose = false -type HutchRabit = { - id : int; - name : string; - five : string; - three : string; - orient : Orientation; - breed : string; - dnaSource : string} /// Load name\tsequence text file of RYSE linkers and return a map let loadRyseLinkers (f:string) = @@ -45,79 +41,72 @@ let extractLinker (s:string ) = if s.StartsWith("Linker_") then s.[7..] else failwithf "ERROR: unable to parse linker name '%s'" s -// FIXME: this should be injected rather than being a mutable global. -let mutable private lookupUrlBase = None - -/// Set the global URL used for looking up parts using RYCOD. -let setPartLookupUrlBase urlBase = lookupUrlBase <- Some urlBase - -let partLookupUrl route = - match lookupUrlBase with - | Some urlBase -> sprintf "%s/%s" urlBase route - | None -> failwith "No global url provided for part lookup." - -// FIXME: this cache is global and mutable and can become stale when GSLC is embedded in a long- -// running application. -let private fetchCache = new ConcurrentDictionary() - -/// Global flag to activate or deactivate caching of part fetch. -/// Long-running clients of GSLC should set this flag to false to avoid building up a large cache -/// that can become stale over time. -let mutable useCache = true - -/// Hutch interaction: fetch part defs from RYCOd service and cache them. -let getPart (route:string) = - - let url = partLookupUrl route - - let lookup () = - let response = Http.Request(url, silentHttpErrors = true) - match response.Body with - | Binary _ -> - failwithf - "Unexpected binary response from %s, with status code %d." - url - response.StatusCode - | Text body -> - if response.StatusCode = 200 then - rycodExample.ThumperRycod.Parse(body) - else - failwithf - "Request to %s failed with status code %d: %s." - url - response.StatusCode - body - - if useCache then - match fetchCache.TryGetValue(url) with - | (true, x) -> x - | (false, _) -> - let result = lookup() - fetchCache.TryAdd(url, result) |> ignore - result - else - lookup() - -/// Get spec for rabit from hutch given rabit id -let getRabit rId = sprintf "rycod/rabit_spec/%d" rId |> getPart - -/// Retrieve a Rabit specifiction from local cache or by making a thumper call. -let getHutchInfoViaWeb ri = - let hr = getRabit ri - let rabit = hr.RabitSpecs.[0] - assert(rabit.Id.StartsWith("R.")) - - {id = int(rabit.Id.[2..]); - name = rabit.Name; - five = (match rabit.UpstreamLink.String with | Some(x) -> x | None -> ""); - three = (match rabit.DownstreamLink.String with | None -> "" | Some(x) -> x); - orient = - match rabit.Direction with - | "FWD" -> FWD - | "REV" -> REV - | _ -> failwithf "inconceivable direction %A\n" rabit.Direction; - breed = rabit.Breed; - dnaSource = sprintf "R%d" ri} +module ThumperPartProvider = + + type ThumperPartProvider(lookupUrlBase: string option, useCache: bool) = + let partLookupUrl route = + match lookupUrlBase with + | Some urlBase -> sprintf "%s/%s" urlBase route + | None -> failwith "No global url provided for part lookup." + let idRegex = Regex("^[Rr]\d+$") + // CAUTION: this cache can become stale when GSLC is embedded in a long-running application. + let fetchCache = new ConcurrentDictionary() + do + () + + + with + interface IPartProvider with + member __.ProvidedArgs() = [] // TODO: move to Amyris private repo and use existing thumperUrl arg. + member x.Configure(_) = x :> IPartProvider // TODO: use command line arg to set lookup URL. + member x.ConfigureFromOptions(_) = x :> IPartProvider + member x.Name = "thumper" + member x.Accept(partId) = idRegex.IsMatch(partId) + member x.Retrieve(partId) = + let route = sprintf "rycod/rabit_spec/%s" partId.[1..] + let url = partLookupUrl route + + let lookup () = + let response = Http.Request(url, silentHttpErrors = true) + match response.Body with + | Binary _ -> + fail <| sprintf + "Unexpected binary response from %s, with status code %d." + url + response.StatusCode + | Text body -> + if response.StatusCode = 200 then + let rycod = rycodExample.ThumperRycod.Parse(body) + + let rabit = rycod.RabitSpecs.[0] + + let linkers = + let five = rabit.UpstreamLink.String |> Option.defaultValue "" + let three = rabit.DownstreamLink.String |> Option.defaultValue "" + Some(five, three) + + ok { + name = rabit.Name + dna = Dna(rabit.DnaElementSpecs.[0].DnaSequence) + linkers = linkers + } + else + fail <| sprintf + "Request to %s failed with status code %d: %s." + url + response.StatusCode + body + + if useCache then + match fetchCache.TryGetValue(url) with + | (true, x) -> ok x + | (false, _) -> + lookup() + >>= (fun part -> + fetchCache.TryAdd(url, part) |> ignore + ok part) + else + lookup() /// Determine which sets of linkers to use for a design let getLinkerSetsForDesign (aIn: DnaAssembly) = @@ -397,12 +386,10 @@ let mapRyseLinkers // Make sure linker is appropriate to precede part hd. // Matters in the case where hd is reuse of a ryse part. match hd.extId with - | Some(x) -> //when x.[0] = 'R' || x.[0] = 'r' -> - let rabitId = int(x) - let h = getHutchInfoViaWeb rabitId + | Some(extId) -> //when x.[0] = 'R' || x.[0] = 'r' -> + let h = fetchPart extId |> returnOrFail - let hFive = sprintf "%s" h.five - let hThree = sprintf "%s" h.three + let hFive, hThree = h.linkers |> Option.defaultValue ("", "") let linkerName = extractLinker linker.description let linkerNameNext = @@ -412,8 +399,8 @@ let mapRyseLinkers let failWithLinkerErrorMsg whichEnd hEnd name = failwithf - "part R%d expects %s linker (%s) and linker (%s) used instead \nERROR:(%s)" - rabitId whichEnd hEnd name errorDesc + "part %s expects %s linker (%s) and linker (%s) used instead \nERROR:(%s)" + extId whichEnd hEnd name errorDesc if phase then if linkerName <> hFive then