25. module SerDe =
let parseNewCommand (command: NewIssueDTO) = {
Title = command.title
Description = command.description
}
let initializeHistory (event: IssueCreatedEvent) =
let (Id idString) = event.Id
{ id = idString
created = { title = event.Title
description = event.Description }
}
26. module API =
let create (dto: NewIssueDTO) =
let command = SerDe.parseNewCommand dto
let event = Domain.createIssue command
let eventLogDTO = SerDe.initializeHistory event
eventLogDTO
27. module API =
let create =
SerDe.parseNewCommand
>> Domain.createIssue
>> SerDe.initializeHistory
28.
29.
30. type public IssuesApp() =
[<FunctionName("CreateIssue")>]
static member CreateIssue (
[<HttpTrigger("post")>]
request : NewIssueDTO,
[<DocumentDB("IssuesDB", "Issues")>]
documents: ICollector<IssueEventLogDTO>) =
let eventLogDTO = API.create request
documents.Add eventLogDTO
eventLogDTO.id
34. type User = User of string
type IssueAssignedToUserEvent = {
User: User
}
type IssueCommentedEvent = {
User: User
Comment: string
}
//type IssueClosedEvent = { }
//type IssueReopenedEvent = { }
35. type AssignIssueCommand = {
User: User
}
let assignIssue (command: AssignIssueCommand) = {
User = command.User
}
40. // Function type is:
// IssueStatus -> AssignIssueCommand -> Result<IssueAssignedEvent,string>
let assignIssue (status: IssueStatus) (command: AssignIssueCommand) =
match status with
| New -> Ok { User = command.User }
| Done -> Error "Can't assign closed issue"
41. let closeIssue (status: IssueStatus) command =
match status with
| New -> Ok ()
| Done -> Error "Issue is already closed"
let reopenIssue (status: IssueStatus) command =
match status with
| Done -> Ok ()
| New -> Error "Can't reopen open issue"
42. type ChangeIssueCommand =
| Assign of AssignIssueCommand
| AddComment of AddCommentCommand
| Close
| Reopen
type IssueChangedEvent =
| Assigned of IssueAssignedToUserEvent
| Commented of IssueCommentedEvent
| Closed
| Reopened
43. module Domain =
// let createIssue ...
let changeIssue (command: ChangeIssueCommand) status =
match command with
| Assign a -> assignIssue status a
| Comment c -> commentIssue status c
| Close -> closeIssue status
| Reopen -> reopenIssue status
44.
45. type IssueEvents = {
Created: IssueCreatedEvent
Changes: IssueChangedEvent list
}
46. let apply status event =
match event with
| Closed -> Done
| Reopened -> New
| _ -> status
let replay (events: IssueEvents): IssueStatus =
List.fold apply New events.Changes
48. module Domain =
// let createIssue ...
let changeIssue (command: Command) (events: IssueEvents) =
let status = replay events
match command with
| Assign a -> assignIssue status a
| Comment c -> commentIssue status c
| Close -> closeIssue status
| Reopen -> reopenIssue status
51. module API =
// let create = …
let change (req: IssueCommandDTO) (log: IssueEventLogDTO) =
let command = SerDe.parseCommand req
let eventLog = SerDe.parseHistory log
let result = Domain.change command eventLog
append log result
httpResponse result
52. let httpResponse result =
match result with
| Ok _ ->
new HttpResponseMessage(HttpStatusCode.OK)
| Error e ->
new HttpResponseMessage(
HttpStatusCode.BadRequest, Content = new StringContent(e))
// httpResponse: Result<T, string> -> HttpResponseMessage
53. type public IssuesApp() =
[<FunctionName("ChangeIssue")>]
static member ChangeIssue (
[<HttpTrigger("put")>]
request : IssueCommandDTO,
[<DocumentDB("IssuesDB", "Issues", Id = "{id}")>]
state: IssueEventLogDTO) =
API.change request state
54.
55. • Concise types
• Choice types (discriminated union)
• Immutability by default
• Explicit concepts
• No nulls, no exceptions
• Make invalid state unrepresentable
• Type as documentation
• Types as guard