Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
73b7f6f
commit 1e0bc83
Showing
7 changed files
with
514 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 17 | ||
VisualStudioVersion = 17.1.32228.430 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3E827421-3A35-4580-A7C0-897CF12FF34D}" | ||
EndProject | ||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "fabulous-minutes.core", "src\fabulous-minutes.core\fabulous-minutes.core.fsproj", "{AC93F0FF-EC45-4766-B975-37F6AB90C3FE}" | ||
EndProject | ||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{721B52E1-B18A-4B87-87F6-F5C320DA17F7}" | ||
ProjectSection(SolutionItems) = preProject | ||
LICENSE = LICENSE | ||
README.md = README.md | ||
EndProjectSection | ||
EndProject | ||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "tests", "tests\tests.fsproj", "{2B266476-22C7-4B47-B7FD-1481171568E8}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{AC93F0FF-EC45-4766-B975-37F6AB90C3FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{AC93F0FF-EC45-4766-B975-37F6AB90C3FE}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{AC93F0FF-EC45-4766-B975-37F6AB90C3FE}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{AC93F0FF-EC45-4766-B975-37F6AB90C3FE}.Release|Any CPU.Build.0 = Release|Any CPU | ||
{2B266476-22C7-4B47-B7FD-1481171568E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{2B266476-22C7-4B47-B7FD-1481171568E8}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{2B266476-22C7-4B47-B7FD-1481171568E8}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{2B266476-22C7-4B47-B7FD-1481171568E8}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(NestedProjects) = preSolution | ||
{AC93F0FF-EC45-4766-B975-37F6AB90C3FE} = {3E827421-3A35-4580-A7C0-897CF12FF34D} | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {6D4AA087-5FE2-4B54-8E22-BEAD557F1678} | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
module DynamicObjExtension | ||
|
||
open Newtonsoft.Json | ||
open DynamicObj | ||
|
||
type DynamicObjConverter() = | ||
inherit JsonConverter<DynamicObj>() | ||
|
||
override this.ReadJson(reader : JsonReader, objectType : System.Type, existingValue : DynamicObj, hasExistingValue:bool, serializer : JsonSerializer) : DynamicObj = | ||
// [Review] | ||
// Naming: sortiert die funktion ein JsonParser array? Wenn ja, sehe ich das nicht - aber ich glaube der name passt nicht. | ||
|
||
// [Review answer] | ||
// umbenannt in 'readJsonParserFieldToDynObj' | ||
|
||
/// The isInit parameter is necessary as the reader starts with the first value. | ||
/// But every iteration thereafter we need to progress the reader to the next value, with reader.next(). | ||
let rec readJsonParserFieldToDynObj (result: obj option) (isInit:bool) = | ||
let addValueToParentList(listObj:obj option) (value:'a) = | ||
/// unbox 'a does not seem to provide any benefit. When comparing output to manually created dyn object, | ||
/// it still needs to be boxed to be equal. | ||
let list = listObj.Value :?> obj seq |> Seq.map (fun x -> unbox<'a> x) |> List.ofSeq | ||
let res = (value::list) |> Seq.ofList | ||
readJsonParserFieldToDynObj (Some res) false | ||
// [Review] | ||
// Nitpicking: auch hier kannste die abfrage evtl schöner machen, soweit ich weiß evaluiert `||` die rechte seite nur wenn die linke false ist: | ||
// let x() = printfn "LOL!"; true | ||
// true || x() -> printet nix | ||
// false || x() -> printet "LOL!" | ||
// | ||
// kannst hier also einfach if isInit || reader.Read() machen | ||
// | ||
// solltest du aber testen | ||
|
||
// [Review answer] | ||
// unit tests sind erfolgreich, guter Punkt! | ||
let next = isInit || reader.Read() | ||
// [Review] | ||
// Nitpicking: next ist schon ein bool, schöner ist `if not next` oder einfach `if next` und die conditionals tauschen | ||
// [Review answer] | ||
// Würde ich normal zustimmen, hier war mir readability wichtig. | ||
// Und ich persönlich fand (if reader.read() = false then result) besser verständlich. | ||
if next = false then | ||
result | ||
else | ||
// [Review] | ||
// An der benennung currentJsonObj siehste hier auch ncohmal, dass der typenname `JsonParser` nicht passt. ist es ein json objekt? auch nich so wirklich. Eigentlich speicherst du nur typ und value vom momentanen token. | ||
// ich würde fast sagen du brauchst den typ garnicht und kannst einfach gegen reader.TokenType matchen und reader.Value verarbeiten. | ||
// [Review answer] | ||
// Sehr smart, nimmt complexity raus, direkt umgesetzt. | ||
let isList = result.IsSome && result.Value :? obj seq | ||
let tokenType = reader.TokenType | ||
let tokenValue = (if isNull reader.Value then None else string reader.Value |> Some) | ||
printfn "%A, %A" tokenType tokenValue | ||
match tokenType with | ||
| JsonToken.StartObject -> | ||
let obj = DynamicObj() | ||
if isList then | ||
let v = readJsonParserFieldToDynObj (Some obj) false | ||
addValueToParentList result v.Value | ||
else | ||
readJsonParserFieldToDynObj (Some obj) false | ||
| JsonToken.EndObject -> | ||
result | ||
| JsonToken.StartArray -> | ||
/// Need to use Sequence to be able to use any casting to and from: obj seq <-> 'a seq | ||
let list: obj seq = Seq.empty | ||
readJsonParserFieldToDynObj (Some list) false | ||
| JsonToken.EndArray -> | ||
let list = result.Value :?> obj seq |> List.ofSeq |> List.rev | ||
Some list | ||
| JsonToken.PropertyName -> | ||
let key = tokenValue.Value | ||
if result.IsNone then failwith "Cannot apply property without parent dyn object." | ||
let parent = | ||
match result.Value with | ||
// [Review] | ||
// Den cast verstehe ich nicht, was genau soll Logger sein, warum kommt das in nem generischen JsonConverter vor? | ||
// [Review answer] | ||
// Das hatte ich vergessen rauszunehmen in der fsx die du angeschaut hast. Hatte den converter nochmal extra | ||
// in eine neue .fsx gemacht, dort war der Fehler schon aufgefallen. | ||
| :? DynamicObj -> | ||
let logger = result.Value :?> DynamicObj | ||
let v = readJsonParserFieldToDynObj None false | ||
logger.SetValue(key, v.Value) | ||
logger |> box | ||
| _ -> failwith "Cannot parse parent type to supported types." | ||
readJsonParserFieldToDynObj (Some parent) false | ||
| JsonToken.String -> | ||
let v = string tokenValue.Value | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
| JsonToken.Integer -> | ||
let v = int tokenValue.Value | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
| JsonToken.Float -> | ||
let v = float tokenValue.Value | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
| JsonToken.Boolean -> | ||
let v = System.Boolean.Parse tokenValue.Value | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
| JsonToken.Null -> | ||
// [Review] | ||
// Null handling bei json ist so ne sache. Da du eh dynamic arbeitest, kannst du das auch nutzen und | ||
// null values einfach weg lassen, dann hast du auch kein Some/None gedöns | ||
// [Review answer] | ||
// nicht sicher was du hier genau meinst oder ich das umsetzen kann | ||
let v = None | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
// TODO! | ||
| JsonToken.Bytes | JsonToken.Date -> | ||
let v = string tokenValue.Value | ||
if isList then | ||
addValueToParentList result v | ||
else | ||
Some v | ||
| any -> | ||
// printfn "CAREFUL! %A" currentJsonObj | ||
readJsonParserFieldToDynObj None false | ||
let res = readJsonParserFieldToDynObj(None) true |> Option.get | ||
match res with | ||
| :? list<obj> as list -> | ||
let loggerList = list | ||
let r = DynamicObj() | ||
r.SetValue("root", loggerList) | ||
r | ||
| :? DynamicObj as root -> | ||
root | ||
| _ -> failwith "Could not parse Result to any supported type." | ||
|
||
override this.WriteJson(writer : JsonWriter, value : DynamicObj, serializer : JsonSerializer) = | ||
let v = | ||
let settings = | ||
let s = JsonSerializerSettings() | ||
s.ReferenceLoopHandling <- ReferenceLoopHandling.Serialize | ||
s | ||
let hasRootArr = value.TryGetValue "root" | ||
if hasRootArr.IsSome then | ||
hasRootArr.Value | ||
|> fun v -> JsonConvert.SerializeObject(v, settings) | ||
else | ||
JsonConvert.SerializeObject(value, settings) | ||
writer.WriteRaw (v) | ||
|
||
let toJson(dynObj:DynamicObj) = | ||
JsonConvert.SerializeObject(dynObj, new DynamicObjConverter()) | ||
|
||
let ofJson(jsonSource:string) = JsonConvert.DeserializeObject<DynamicObj>(jsonSource, new DynamicObjConverter()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
namespace FabulousMinutes.Core | ||
|
||
open System | ||
open DynamicObj | ||
|
||
module Template = | ||
|
||
let dynamicAccess (dynObject:DynamicObj) (accessStr:string) = | ||
let toDynArr = accessStr.Split([|"."|], StringSplitOptions.RemoveEmptyEntries) | ||
let rec access (ind:int) (dynArr:string []) result = | ||
if ind >= dynArr.Length then | ||
result | ||
elif ind <> 0 && result = None then | ||
None | ||
else | ||
let parentObj = if ind = 0 then dynObject else box result.Value :?> DynamicObj | ||
let next = parentObj.TryGetValue(dynArr.[ind]) | ||
access (ind+1) dynArr next | ||
access 0 toDynArr None | ||
|
||
/// 1. negative lookbehind: (?<!(/|\\)) -> No / or \ before { | ||
/// 2. must start with: { | ||
/// 3. capture named group 'value' : (?<value>.+?(?!(/|\\))); careful \<value> does not show as comment, better look at code. | ||
/// 4. group contains any number of wildcard characters except { AND }, minimum 1 but as few as possible: [^\{}]+? | ||
/// 5. negative lookahead: (?!(/|\\)) -> No / or \ before } | ||
/// 6. must end with: } | ||
[<Literal>] | ||
let private Pattern = @"(?<!(/|\\)){(?<value>[^\{}]+?(?!(/|\\)))}" | ||
|
||
open System.Text.RegularExpressions | ||
|
||
let getDynamicAccessStrings (input: string) = [| for i in Regex.Matches(input, Pattern) -> i |] | ||
|
||
let readDynObjInFormatString(dynObj:DynamicObj,formatString:string) = | ||
/// Need replacerList to store arbitrary guids and actual dynamic access values. | ||
/// The Guids are used as temporary replacements to remove escaped curly braces, without accidentally touching any inserted dynamic values. | ||
let mutable replacerList: (string*string) list = [] | ||
let evaluator = | ||
MatchEvaluator (fun m -> | ||
let dynAccessResult = dynamicAccess (dynObj) m.Groups.["value"].Value | ||
let dynAccessResultString = | ||
if dynAccessResult.IsSome then | ||
dynAccessResult.Value.ToString() | ||
else | ||
"None" | ||
let newGuid = System.Guid.NewGuid().ToString() | ||
// save both guid and actual value in replacerList. | ||
replacerList <- (newGuid,dynAccessResultString)::replacerList | ||
// return guid to replace dynamic access string | ||
newGuid | ||
) | ||
let removeEscapedCurlyBraces(str:string) = | ||
Regex.Replace(str, @"(\\{|/{)", @"{") | ||
|> fun x -> Regex.Replace(x, @"(\\}|/})", @"}") | ||
let replaceTempGuids(str:string) = | ||
let mutable res = str | ||
replacerList |> List.iter (fun (guid,value) -> | ||
res <- Regex.Replace(res, guid, value) | ||
) | ||
res | ||
// replace dyn access string with random guids, stored with actual values in replacerList | ||
Regex.Replace(formatString, Pattern, evaluator) | ||
// Update escaped curly braces to normal curly braces | ||
|> removeEscapedCurlyBraces | ||
// replace guids with actual dynamic access values | ||
|> replaceTempGuids | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<RootNamespace>fabulous_minutes.core</RootNamespace> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="DynamicObjExtension.fs" /> | ||
<Compile Include="Template.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="DynamicObj" Version="1.0.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module tests | ||
open Expecto | ||
|
||
[<EntryPoint>] | ||
let main argv = | ||
Tests.runTestsInAssembly defaultConfig argv |
Oops, something went wrong.