Library which contains a Command types and basic modules.
Add following into paket.references
Alma.Command
There are 2 generic command types:
type Command<'MetaData, 'CommandData> =
| Synchronous of SynchronousCommand<'MetaData, 'CommandData>
| Asynchronous of AsynchronousCommand<'MetaData, 'CommandData>type SynchronousCommand<'MetaData, 'CommandData> = {
Schema: int
Id: CommandId
CorrelationId: CorrelationId
CausationId: CausationId
Timestamp: string
TimeToLive: TimeToLive
AuthenticationBearer: AuthenticationBearer
Request: Request
Reactor: Reactor
Requestor: Requestor
MetaData: 'MetaData
Data: Data<'CommandData>
}type AsynchronousCommand<'MetaData, 'CommandData> = {
Schema: int
Id: CommandId
CorrelationId: CorrelationId
CausationId: CausationId
Timestamp: string
TimeToLive: TimeToLive
AuthenticationBearer: AuthenticationBearer
Request: Request
Reactor: Reactor
Requestor: Requestor
ReplyTo: ReplyTo
MetaData: 'MetaData
Data: Data<'CommandData>
}Create your own command module:
[<RequireQualifiedAccess>]
module MyCommand =
open System
open Alma.Command
open Alma.Command.CommonSerializer
open Alma.ServiceIdentification
let private request = "my_command_name" |> Request.create |> Result.orFail
type CommandData = {
Service: DataItem<Service>
}
type Command = private Command of SynchronousCommand<MetaData, CommandData>
[<RequireQualifiedAccess>]
module Command =
let internal command (Command command) = command
let create currentApplication reactor authentication ttl commit service =
let now = DateTime.Now
let commandId = Guid.NewGuid() |> CommandId
let commandData = {
Service = (service, "string") |> DataItem.createWithType
}
Command {
Schema = 1
Id = commandId
CorrelationId = CorrelationId.fromCommandId commandId
CausationId = CausationId.fromCommandId commandId
Timestamp = now |> formatDateTime
TimeToLive = ttl
AuthenticationBearer = authentication
Request = request
Reactor = reactor
Requestor = Requestor currentApplication
MetaData = OnlyCreatedAt (CreatedAt now)
Data = Data commandData
}
//
// Serialize DTO
//
type DataDto = {
service: DataItemDto<string>
}
type CommandDto = CommandDto<MetaDataDto.OnlyCreatedAt, DataDto>
[<RequireQualifiedAccess>]
module private Dto =
let private serializeData data =
{
service = data.Service |> DataItemDto.serialize (Service.concat "-")
}
let fromCommand command =
Command.Synchronous command
|> Command.toDto
MetaDataDto.serialize
(serializeData >> Ok)
// Public DTO functions
let serialize: Serialize<Command, MetaData, CommandData, CommandDto> =
fun (Command command) ->
command |> Dto.fromCommandThen use your command:
open Alma.Command
asyncResult { // AsyncResult<CommandResponse, string>
let myCommand = // MyCommand.Command
service
|> MyCommand.Command.create
currentApplication.Box
(Reactor (api.Identification |> BoxPattern.ofServiceIdentification))
api.Authentication
(TimeToLive.ofSeconds 2)
let! myCommandDto = // Alma.Command.CommandDto
myCommand
|> MyCommand.serialize
|> AsyncResult.ofResult <@> DtoError.format
let serializedCommand = // string
myCommandDto
|> CommandDto.serialize Serialize.toJson
let! response = // string
serializedCommand
|> Api.sendCommand api
|> AsyncResult.ofAsync
let! commandResponse = // Alma.Command.CommandResponse<NotParsed, NotParsed>
response
|> CommandResponse.parse <@> (sprintf "Error: %A")
|> AsyncResult.ofResult
return commandData
}Command handler is a common way of handling commands.
Before executing a command, there are a few validations for a given command.
-
TTL
- check, that Command.timestamp + Command.ttl is still valid (
validFrom <= now && now <= validTo) - it should end with
408 Timeout, if it is not valid
[<- valid ->] ┌───────────┐ │ │ --------------------------------------> t │ │ timestamp ttl - check, that Command.timestamp + Command.ttl is still valid (
-
Reactor
- Reactor is matched based on Command.reactor pattern
- if a reactor (current command handler) is not matching a Command.reactor pattern, it ends with an Error
Spot is determine based on a Command.reactor and Command.requestor as follows
-
Specified by reactor
- if a Command.reactor (box pattern) contains a predefined Spot, it is used as is
Command
{ ... "reactor": { ... "zone": "my", "bucket": "data" }, "requestor": { ... "zone": "some", "bucket": "bucket" }, ... }Spot
{ "zone": "my", "bucket": "data" } -
Unspecified by reactor
- if a Command.reactor (box pattern) contains a
*(Any) in the Spot, a Command.requestor.spot is used
Command
{ ... "reactor": { ... "zone": "*", "bucket": "*" }, "requestor": { ... "zone": "some", "bucket": "bucket" }, ... }Spot
{ "zone": "some", "bucket": "bucket" } - if a Command.reactor (box pattern) contains a
NOTE: It applies for Spot, Zone and Bucket in the same way
Command
{
...
"reactor": {
...
"zone": "all",
"bucket": "*"
},
"requestor": {
...
"zone": "some",
"bucket": "bucket"
},
...
}Spot
{
"zone": "all",
"bucket": "bucket"
}- Increment version in
Command.fsproj - Update
CHANGELOG.md - Commit new version and tag it
./build.sh build./build.sh -t tests