Skip to content

Commit

Permalink
Initial set up 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
Freymaurer committed Apr 26, 2022
1 parent 73b7f6f commit 1e0bc83
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 0 deletions.
42 changes: 42 additions & 0 deletions fabulous-minutes.sln
@@ -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
162 changes: 162 additions & 0 deletions src/fabulous-minutes.core/DynamicObjExtension.fs
@@ -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())
67 changes: 67 additions & 0 deletions src/fabulous-minutes.core/Template.fs
@@ -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

18 changes: 18 additions & 0 deletions src/fabulous-minutes.core/fabulous-minutes.core.fsproj
@@ -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>
6 changes: 6 additions & 0 deletions tests/Main.fs
@@ -0,0 +1,6 @@
module tests
open Expecto

[<EntryPoint>]
let main argv =
Tests.runTestsInAssembly defaultConfig argv

0 comments on commit 1e0bc83

Please sign in to comment.