Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: StreamId #100

Merged
merged 9 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `StreamId`: type-tagged wrapper for the streamId portion of a `StreamName` [#100](https://github.com/jet/FsCodec/pull/100)
- `StreamName.Split`: Splits a StreamName into its `{category}` and `{streamId}` portions, using `StreamId` for the latter. Replaces `CategoryAndId` [#100](https://github.com/jet/FsCodec/pull/100)
- `StreamName.tryFind`: Helper to implement `Stream.tryDecode` / `Reactions.For` pattern (to implement validation of StreamId format when parsing `StreamName`s). (See README) [#100](https://github.com/jet/FsCodec/pull/100)
- `StreamName.Category`: covers aspects of `StreamName` pertaining to the `{category}` portion (mainly moved from `StreamName`.* equivalents; see Changed) [#100](https://github.com/jet/FsCodec/pull/100)

### Changed

- `StreamName`: breaking changes to reflect introduction of strongly typed `StreamId` [#100](https://github.com/jet/FsCodec/pull/100)
- `StreamName`: renames: `trySplitCategoryAndStreamId` -> `Internal.tryParse`; `splitCategoryAndStreamId` -> `split`; `CategoryAndId` -> `Split`; `Categorized|NotCategorized`-> `Internal`.*; `category`->`Category.ofStreamName`, `IdElements` -> `StreamId.Parse` [#100](https://github.com/jet/FsCodec/pull/100)

### Removed

- `StreamName`., `CategoryAndIds`: See new `StreamId`, `StreamId.Elements` [#100](https://github.com/jet/FsCodec/pull/100)

### Fixed

<a name="3.0.0-rc.10"></a>
Expand Down
510 changes: 277 additions & 233 deletions README.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/FsCodec.NewtonsoftJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ module private Union =

/// Serializes a discriminated union case with a single field that is a
/// record by flattening the record fields to the same level as the discriminator
type UnionConverter private (discriminator : string, ?catchAllCase) =
type UnionConverter private (discriminator: string, ?catchAllCase) =
inherit JsonConverter()

new() = UnionConverter("case", ?catchAllCase=None)
new(discriminator: string) = UnionConverter(discriminator, ?catchAllCase=None)
new(discriminator: string, catchAllCase: string) = UnionConverter(discriminator, ?catchAllCase=match catchAllCase with null -> None | x -> Some x)
new() = UnionConverter("case", ?catchAllCase = None)
new(discriminator: string) = UnionConverter(discriminator, ?catchAllCase = None)
new(discriminator: string, catchAllCase: string) = UnionConverter(discriminator, ?catchAllCase = match catchAllCase with null -> None | x -> Some x)

override _.CanConvert (t : Type) = Union.isUnion t
override _.CanConvert(t : Type) = Union.isUnion t

override _.WriteJson(writer : JsonWriter, value : obj, serializer : JsonSerializer) =
let union = Union.getInfo (value.GetType())
Expand Down Expand Up @@ -117,7 +117,7 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =

writer.WriteEndObject()

override _.ReadJson(reader : JsonReader, t : Type, _ : obj, serializer : JsonSerializer) =
override _.ReadJson(reader: JsonReader, t: Type, _: obj, serializer: JsonSerializer) =
let token = JToken.ReadFrom reader
if token.Type <> JTokenType.Object then raise (FormatException(sprintf "Expected object token, got %O" token.Type))
let inputJObject = token :?> JObject
Expand Down
1 change: 1 addition & 0 deletions src/FsCodec/FsCodec.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<Compile Include="FsCodec.fs" />
<Compile Include="Codec.fs" />
<Compile Include="StreamId.fs" />
<Compile Include="StreamName.fs" />
</ItemGroup>

Expand Down
100 changes: 100 additions & 0 deletions src/FsCodec/StreamId.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Represents the second half of a canonical StreamName, i.e., the streamId in "{categoryName}-{streamId}"
// Low-level helpers for composing and rendering StreamId values; prefer the ones in the Equinox namespace
namespace FsCodec

open FSharp.UMX

/// Represents the second half of a canonical StreamName, i.e., the streamId in "{categoryName}-{streamId}"
type StreamId = string<streamId>
and [<Measure>] streamId

/// Helpers for composing and rendering StreamId values
module StreamId =

/// Any string can be a StreamId; parse/dec/Elements.split will judge whether it adheres to a valid form
let create: string -> StreamId = UMX.tag

/// Render as a string for external use
let toString: StreamId -> string = UMX.untag

module Element =

let [<Literal>] Separator = '_' // separates {subId1_subId2_..._subIdN}

/// Throws if a candidate id element includes a '_', is null, or is empty
let inline validate (raw: string) =
if raw |> System.String.IsNullOrEmpty then invalidArg "raw" "Element must not be null or empty"
if raw.IndexOf Separator <> -1 then invalidArg "raw" "Element may not contain embedded '_' symbols"

module Elements =

let [<Literal>] Separator = "_"

/// Create a StreamId, trusting the input to be well-formed (see the gen* functions for composing with validation)
let trust (raw: string): StreamId = UMX.tag raw

/// Creates from exactly one fragment. Throws if the fragment embeds a `_`, are `null`, or is empty
let parseExactlyOne (rawFragment: string): StreamId =
Element.validate rawFragment
trust rawFragment

/// Combines streamId fragments. Throws if any of the fragments embed a `_`, are `null`, or are empty
let compose (rawFragments: string[]): StreamId =
rawFragments |> Array.iter Element.validate
System.String.Join(Separator, rawFragments) |> trust

let private separator = [| Element.Separator |]
/// Splits a streamId into its constituent fragments
let split (x: StreamId): string[] =
(toString x).Split separator
/// Splits a streamId into its constituent fragments
let (|Split|): StreamId -> string[] = split

/// Helpers to generate StreamIds given a number of individual id to string mapper functions
[<AbstractClass; Sealed>]
type Gen private () =

/// Generate a StreamId from a single application-level id, given a rendering function that maps to a non empty fragment without embedded `_` chars
static member Map(f: 'a -> string) = System.Func<'a, StreamId>(fun id -> f id |> Elements.parseExactlyOne)
/// Generate a StreamId from a tuple of application-level ids, given 2 rendering functions that map to a non empty fragment without embedded `_` chars
static member Map(f, f2) = System.Func<'a, 'b, StreamId>(fun id1 id2 -> Elements.compose [| f id1; f2 id2 |])
/// Generate a StreamId from a triple of application-level ids, given 3 rendering functions that map to a non empty fragment without embedded `_` chars
static member Map(f1, f2, f3) = System.Func<'a, 'b, 'c, StreamId>(fun id1 id2 id3 -> Elements.compose [| f1 id1; f2 id2; f3 id3 |])
/// Generate a StreamId from a 4-tuple of application-level ids, given 4 rendering functions that map to a non empty fragment without embedded `_` chars
static member Map(f1, f2, f3, f4) = System.Func<'a, 'b, 'c, 'd, StreamId>(fun id1 id2 id3 id4 -> Elements.compose [| f1 id1; f2 id2; f3 id3; f4 id4 |])

/// Generate a StreamId from a single application-level id, given a rendering function that maps to a non empty fragment without embedded `_` chars
let gen (f: 'a -> string): 'a -> StreamId = Gen.Map(f).Invoke
/// Generate a StreamId from a tuple of application-level ids, given two rendering functions that map to a non empty fragment without embedded `_` chars
let gen2 f1 f2: 'a * 'b -> StreamId = Gen.Map(f1, f2).Invoke
/// Generate a StreamId from a triple of application-level ids, given three rendering functions that map to a non empty fragment without embedded `_` chars
let gen3 f1 f2 f3: 'a * 'b * 'c -> StreamId = Gen.Map(f1, f2, f3).Invoke
/// Generate a StreamId from a 4-tuple of application-level ids, given four rendering functions that map to a non empty fragment without embedded `_` chars
let gen4 f1 f2 f3 f4: 'a * 'b * 'c * 'd -> StreamId = Gen.Map(f1, f2, f3, f4).Invoke

/// Validates and extracts the StreamId into a single fragment value
/// Throws if the item embeds a `_`, is `null`, or is empty
let parseExactlyOne (x: StreamId): string = toString x |> Elements.parseExactlyOne |> toString
/// Validates and extracts the StreamId into a single fragment value
/// Throws if the item embeds a `_`, is `null`, or is empty
let (|Parse1|) (x: StreamId): string = parseExactlyOne x

/// Splits a StreamId into the specified number of fragments.
/// Throws if the value does not adhere to the expected fragment count.
let parse count (x: StreamId): string[] =
let xs = Elements.split x
if xs.Length <> count then
invalidArg "x" (sprintf "StreamId '{%s}' must have {%d} elements, but had {%d}." (toString x) count xs.Length)
xs
/// Splits a StreamId into an expected number of fragments.
/// Throws if the value does not adhere to the expected fragment count.
let (|Parse|) count: StreamId -> string[] = parse count

/// Extracts a single fragment from the StreamId. Throws if the value is composed of more than one item.
let dec f (x: StreamId) = parseExactlyOne x |> f
/// Extracts 2 fragments from the StreamId. Throws if the value does not adhere to that expected form.
let dec2 f1 f2 (x: StreamId) = let xs = parse 2 x in struct (f1 xs[0], f2 xs[1])
/// Extracts 3 fragments from the StreamId. Throws if the value does not adhere to that expected form.
let dec3 f1 f2 f3 (x: StreamId) = let xs = parse 3 x in struct (f1 xs[0], f2 xs[1], f3 xs[2])
/// Extracts 4 fragments from the StreamId. Throws if the value does not adhere to that expected form.
let dec4 f1 f2 f3 f4 (x: StreamId) = let xs = parse 4 x in struct (f1 xs[0], f2 xs[1], f3 xs[2], f4 xs[3])
Loading