SAFE Stack の ToDo アプリはデータを保存できるようにする続き。

ファイル操作はサーバでおこない、クライアントに API を提供する形になる。 API のシグネチャを src/Shared/Shared.fs に追加する。

diff --git a/src/Shared/Shared.fs b/src/Shared/Shared.fs
--- a/src/Shared/Shared.fs
+++ b/src/Shared/Shared.fs
@@ -80,4 +80,5 @@ module Route =
 type ITodosApi =
     { getTodos: unit -> Async<Todo list>
       addTodo: Todo -> Async<Todo>
-      moveTodo: int * MoveDstPosition -> Async<Todo List> }
+      moveTodo: int * MoveDstPosition -> Async<Todo List>
+      saveTodos: unit -> Async<Todo list> }

そして、FSharp.Json (link) や準備したユーティリティー (link) を使って目的の機能を実現するように src/Server/Server.fs を変更する。 Storage クラスに JSON ファイルの読み書きを追加した。

diff --git a/src/Server/Server.fs b/src/Server/Server.fs
index 9a01e94..9c24f82 100644
--- a/src/Server/Server.fs
+++ b/src/Server/Server.fs
@@ -6,10 +6,38 @@ open Saturn

 open Serilog
 open Giraffe.SerilogExtensions
+open FSharp.Json

 open Shared
+open ServerUtil
+
+let dataDirEnvName = "TODO_DATA_DIR"
+let defaultDataSubdir = "TodoApp"
+let dataBasename = "todos.json"
+
+let writeTodoListToJsonFile (filePath: string) (todos: Todo list) =
+    let json = Json.serialize todos
+    System.IO.File.WriteAllText (filePath, json)
+
+let readTodoListFromJsonFile (filePath: string) : Todo list =
+    let text = System.IO.File.ReadAllText filePath
+    Json.deserialize<Todo list> text
+
+let dataDir () =
+    let dataSubdir =
+        match EnvUtil.getEnv dataDirEnvName |> StringUtil.strip with
+        | "" -> StringUtil.strip defaultDataSubdir
+        | x  -> x
+    if IoUtil.isAbsPath dataSubdir then
+        dataSubdir
+    else
+        (IoUtil.docRoot ()) + IoUtil.dirSepStr + dataSubdir
+
-type Storage() =
+type Storage (dirPath_arg: string) =
+    let dirPath = dirPath_arg
+    let () = IoUtil.assureDir dirPath
+    let filePath = dirPath + IoUtil.dirSepStr + dataBasename

     // ResizeArray is System.Collections.Generic.List
     let todos = ResizeArray<_>()

@@ -35,21 +63,39 @@ type Storage() =
         TodoResizeArray.move todos srcIdx dstPos
         Ok (List.ofSeq todos)

+    member __.SaveTodos () =
+        IoUtil.assureDir dirPath
+        let todoList = List.ofSeq todos
+        writeTodoListToJsonFile filePath todoList
+        Ok todoList
+
+    member __.LoadTodos () =
+        if not (IoUtil.checkFile filePath) then
+            Error $"file dose not exist: {filePath}"
+        else
+            let newTodoList = readTodoListFromJsonFile filePath
+            todos.Clear ()
+            todos.AddRange (newTodoList)
+            Ok newTodoList
+
+    member this.ResumeTodos () =
+        match this.LoadTodos () with
+        | Ok _ -> true
+        | Error msg -> false
+
 Log.Logger <- LoggerConfiguration()
               .Destructure.FSharpTypes()
               .WriteTo.Console()
               .CreateLogger()

-let storage = Storage()
+let storage = dataDir () |> Storage

-storage.AddTodo(Todo.create "Create new SAFE project")
-|> ignore
-
-storage.AddTodo(Todo.create "Write your app")
-|> ignore
-
-storage.AddTodo(Todo.create "Ship it !!!")
-|> ignore
+if storage.ResumeTodos () then
+    ()
+else
+    storage.AddTodo (Todo.create "Create new SAFE project") |> ignore
+    storage.AddTodo (Todo.create "Write your app") |> ignore
+    storage.AddTodo (Todo.create "Ship it !!!") |> ignore

 let todosApi (logger: ILogger) =
     { getTodos =
@@ -80,7 +126,19 @@ let todosApi (logger: ILogger) =
                       | Error e -> failwith e
                   logger.Information "moveTodo: end"
                   return newTodoList
-              } }
+              }
+      saveTodos =
+          fun () ->
+              async {
+                  logger.Information "saveTodos: begin"
+                  let newTodoList =
+                      match storage.SaveTodos () with
+                      | Ok newTodoList -> newTodoList
+                      | Error e -> failwith e
+                  logger.Information "saveTodos: end"
+                  return newTodoList
+              }
+    }

 open Microsoft.AspNetCore.Http