Skip to content
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
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
│ ├── CosmosResponse.fs – response type wrappers
│ ├── TaskSeq.fs – TaskSeq integration
│ └── UniqueKey.fs – unique key helpers
├── src/Shared/ – source files that every F# project under src and tests compiles
│ └── ValueCollections.fs – `Seq`, `List` and `Array` functions that return `voption` and struct tuples
├── tests/Cosmos.Tests/ – MSTest integration test project
├── tests/Cosmos.Tests.Infrastructure/ – shared test fixtures, emulator settings and assertion helpers
├── build/ – FAKE build scripts
Expand Down Expand Up @@ -71,6 +73,7 @@ Agents discover servers from #file:'.mcp.json'; these are only hints on when to

* Prefer `voption` (`ValueSome`/`ValueNone`) over `option`. Fields, members, parameters and values shared between threads included: none of them is a reason to pick `option`. Exception: when an API hands you `'T option` and has no `voption` counterpart, unwrap it with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules.
* The mirror case, an API that *takes* `'T option` (an optional argument `?name = …`, a field typed `'T option`): stay in `ValueOption` through the whole chain and convert once, last – `x |> ValueOption.bind _.Value |> ValueOption.toOption`, never `x |> ValueOption.toOption |> Option.bind _.Value`.
* The `Seq`, `List` and `Array` functions that return an option in FSharp.Core (`tryHead`, `tryLast`, `tryItem`, `tryExactlyOne`, `tryFind`, `tryFindBack`, `tryFindIndex`, `tryFindIndexBack`, `tryPick`) return a `voption` here, `tryPick` takes a chooser that returns one, and `zip` returns struct tuples: #file:'src/Shared/ValueCollections.fs' shadows them in the namespace `FSharp.Azure.Cosmos` of every F# project under `src` and `tests`, so they are in effect in the code of that namespace and in every file that opens it; a file in another namespace, a child namespace such as `FSharp.Azure.Cosmos.Tests` included, opens `FSharp.Azure.Cosmos` to get them. Match their results with `ValueSome`/`ValueNone`; when another function of this kind is needed, add its shadow to that file instead of converting at the call site. The build project keeps the functions of FSharp.Core.
* Prefer `struct ('T1 * 'T2)` over reference tuples, and anonymous struct records (`struct {| ... |}`) over tuples for return types of public functions and methods.
* Never group with `Seq.groupBy` – use `ToLookup` from `System.Linq`. It groups once into an `ILookup<'Key, 'T>` instead of re-grouping on every enumeration and does not allocate a tuple per group. Pass a lambda (`xs.ToLookup (fun x -> keyOf x)`), not a bare function value.
* When casting sequence items use `Seq.cast<TargetType>` instead of `Seq.map (fun item -> item :> TargetType)`.
Expand Down
10 changes: 10 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ See https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-director
<PackageReference Include="FSharp.Core" />
</ItemGroup>

<!--
Every F# project of the library and of its tests compiles the collection helpers first. They shadow the functions
of Seq, List and Array that return an option or reference tuples with functions that return a voption or struct
tuples, for the code of the namespace FSharp.Azure.Cosmos and for every file that opens it. The build project is
left out: its scripts were generated against the functions of FSharp.Core.
-->
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.fsproj' And '$(MSBuildProjectName)' != 'build'">
<Compile Include="$(MSBuildThisFileDirectory)src/Shared/ValueCollections.fs" Link="Shared/ValueCollections.fs" />
</ItemGroup>

<!--<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="BinaryDefense.FSharp.Analyzers.Hashing" PrivateAssets="all" />
<PackageReference Include="FSharp.Compiler.Service" PrivateAssets="all" />
Expand Down
4 changes: 2 additions & 2 deletions src/Cosmos/Read.fs
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ type FeedIteratorExtensions private () =

static let seqToReadResult (items : 'T seq) =
match items |> Seq.tryHead with
| Some item -> Ok item
| None -> NotFound "Item not found"
| ValueSome item -> Ok item
| ValueNone -> NotFound "Item not found"

[<Extension>]
static member FirstAsync (iterator : FeedIterator<'T>, [<Optional>] cancellationToken : CancellationToken) = task {
Expand Down
269 changes: 269 additions & 0 deletions src/Shared/ValueCollections.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/// <summary>
/// The functions of <see cref="T:Microsoft.FSharp.Collections.SeqModule"/>,
/// <see cref="T:Microsoft.FSharp.Collections.ListModule"/> and <see cref="T:Microsoft.FSharp.Collections.ArrayModule"/>
/// that return an <see cref="T:Microsoft.FSharp.Core.FSharpOption`1"/> or reference tuples, shadowed by functions that
/// return a <see cref="T:Microsoft.FSharp.Core.FSharpValueOption`1"/> or struct tuples, so that looking an element up or
/// pairing two collections allocates no object per result.
/// </summary>
/// <remarks>
/// <para>
/// Every F# project under <c>src</c> and <c>tests</c> compiles this file first, through the root
/// <c>Directory.Build.props</c>, and gets its own internal copy. The module opens itself in the namespace
/// <c>FSharp.Azure.Cosmos</c>: the shadows are in effect in the code of that namespace and in every file that opens it.
/// A file in another namespace needs <c>open FSharp.Azure.Cosmos</c>, and so does one in a child namespace such as
/// <c>FSharp.Azure.Cosmos.Tests</c>, because a child namespace does not open the modules of its parent.
/// </para>
/// <para>
/// A shadow means what the FSharp.Core function of the same name means, and that function stays reachable by its full
/// name: <see cref="M:Microsoft.FSharp.Collections.SeqModule.TryHead``1(System.Collections.Generic.IEnumerable{``0})"/>
/// is written <c>Microsoft.FSharp.Collections.Seq.tryHead</c>. Functions that only FSharp.Core has, such as
/// <see cref="M:Microsoft.FSharp.Collections.SeqModule.Map``2(Microsoft.FSharp.Core.FSharpFunc{``0,``1},System.Collections.Generic.IEnumerable{``0})"/>,
/// resolve to FSharp.Core as before. A function that is missing here is added here, not converted at its call site.
/// </para>
/// </remarks>
[<AutoOpen>]
module internal FSharp.Azure.Cosmos.ValueCollections

open System.Linq

[<RequireQualifiedAccess>]
module Array =

/// The first element, or no value when the array is empty.
let tryHead (array : 'T array) : 'T voption = if array.Length = 0 then ValueNone else ValueSome array[0]

/// The last element, or no value when the array is empty.
let tryLast (array : 'T array) : 'T voption =
if array.Length = 0 then
ValueNone
else
ValueSome array[array.Length - 1]

/// The element at the index, or no value when the index is outside the array.
let tryItem (index : int) (array : 'T array) : 'T voption =
if index < 0 || index >= array.Length then
ValueNone
else
ValueSome array[index]

/// The only element, or no value when the array is empty or has more than one element.
let tryExactlyOne (array : 'T array) : 'T voption = if array.Length = 1 then ValueSome array[0] else ValueNone

/// The index of the first element for which the predicate holds, or no value when it holds for none.
let tryFindIndex (predicate : 'T -> bool) (array : 'T array) : int voption =
let mutable index = 0
let mutable found = ValueNone

while found.IsNone && index < array.Length do
if predicate array[index] then
found <- ValueSome index

index <- index + 1

found

/// The index of the last element for which the predicate holds, or no value when it holds for none.
let tryFindIndexBack (predicate : 'T -> bool) (array : 'T array) : int voption =
let mutable index = array.Length - 1
let mutable found = ValueNone

while found.IsNone && index >= 0 do
if predicate array[index] then
found <- ValueSome index

index <- index - 1

found

/// The first element for which the predicate holds, or no value when it holds for none.
let tryFind (predicate : 'T -> bool) (array : 'T array) : 'T voption =
match tryFindIndex predicate array with
| ValueSome index -> ValueSome array[index]
| ValueNone -> ValueNone

/// The last element for which the predicate holds, or no value when it holds for none.
let tryFindBack (predicate : 'T -> bool) (array : 'T array) : 'T voption =
match tryFindIndexBack predicate array with
| ValueSome index -> ValueSome array[index]
| ValueNone -> ValueNone

/// The first value that the chooser gives for an element, or no value when it gives none.
let tryPick (chooser : 'T -> 'U voption) (array : 'T array) : 'U voption =
let mutable index = 0
let mutable picked = ValueNone

while picked.IsNone && index < array.Length do
picked <- chooser array[index]
index <- index + 1

picked

/// The elements of two arrays of the same length, paired by index; arrays of different lengths are an error, as for
/// the function of FSharp.Core.
let zip (first : 'T1 array) (second : 'T2 array) : struct ('T1 * 'T2) array =
Array.map2 (fun left right -> struct (left, right)) first second

[<RequireQualifiedAccess>]
module List =

/// The first element, or no value when the list is empty.
let tryHead (list : 'T list) : 'T voption =
match list with
| head :: _ -> ValueSome head
| [] -> ValueNone

/// The last element, or no value when the list is empty.
let rec tryLast (list : 'T list) : 'T voption =
match list with
| [] -> ValueNone
| [ last ] -> ValueSome last
| _ :: tail -> tryLast tail

/// The element at the index, or no value when the index is outside the list.
let rec tryItem (index : int) (list : 'T list) : 'T voption =
match list with
| head :: _ when index = 0 -> ValueSome head
| _ :: tail when index > 0 -> tryItem (index - 1) tail
| _ -> ValueNone

/// The only element, or no value when the list is empty or has more than one element.
let tryExactlyOne (list : 'T list) : 'T voption =
match list with
| [ only ] -> ValueSome only
| _ -> ValueNone

/// The first element for which the predicate holds, or no value when it holds for none.
let rec tryFind (predicate : 'T -> bool) (list : 'T list) : 'T voption =
match list with
| [] -> ValueNone
| head :: _ when predicate head -> ValueSome head
| _ :: tail -> tryFind predicate tail

/// The last element for which the predicate holds, or no value when it holds for none.
let tryFindBack (predicate : 'T -> bool) (list : 'T list) : 'T voption = list |> List.toArray |> Array.tryFindBack predicate

/// The index of the first element for which the predicate holds, or no value when it holds for none.
let tryFindIndex (predicate : 'T -> bool) (list : 'T list) : int voption =
let rec search index remaining =
match remaining with
| [] -> ValueNone
| head :: _ when predicate head -> ValueSome index
| _ :: tail -> search (index + 1) tail

search 0 list

/// The index of the last element for which the predicate holds, or no value when it holds for none.
let tryFindIndexBack (predicate : 'T -> bool) (list : 'T list) : int voption =
list |> List.toArray |> Array.tryFindIndexBack predicate

/// The first value that the chooser gives for an element, or no value when it gives none.
let rec tryPick (chooser : 'T -> 'U voption) (list : 'T list) : 'U voption =
match list with
| [] -> ValueNone
| head :: tail ->
match chooser head with
| ValueSome _ as picked -> picked
| ValueNone -> tryPick chooser tail

/// The elements of two lists of the same length, paired by position; lists of different lengths are an error, as for
/// the function of FSharp.Core.
let zip (first : 'T1 list) (second : 'T2 list) : struct ('T1 * 'T2) list =
List.map2 (fun left right -> struct (left, right)) first second

[<RequireQualifiedAccess>]
module Seq =

/// The first element, or no value when the sequence is empty.
let tryHead (source : 'T seq) : 'T voption =
use enumerator = source.GetEnumerator ()

if enumerator.MoveNext () then
ValueSome enumerator.Current
else
ValueNone

/// The last element, or no value when the sequence is empty.
let tryLast (source : 'T seq) : 'T voption =
use enumerator = source.GetEnumerator ()

if enumerator.MoveNext () then
let mutable last = enumerator.Current

while enumerator.MoveNext () do
last <- enumerator.Current

ValueSome last
else
ValueNone

/// The element at the index, or no value when the index is negative or the sequence ends before it.
let tryItem (index : int) (source : 'T seq) : 'T voption =
if index < 0 then
ValueNone
else
use enumerator = source.GetEnumerator ()
let mutable remaining = index
let mutable reached = enumerator.MoveNext ()

while reached && remaining > 0 do
remaining <- remaining - 1
reached <- enumerator.MoveNext ()

if reached then ValueSome enumerator.Current else ValueNone

/// The only element, or no value when the sequence is empty or has more than one element.
let tryExactlyOne (source : 'T seq) : 'T voption =
use enumerator = source.GetEnumerator ()

if enumerator.MoveNext () then
let only = enumerator.Current
if enumerator.MoveNext () then ValueNone else ValueSome only
else
ValueNone

/// The first element for which the predicate holds, or no value when it holds for none.
let tryFind (predicate : 'T -> bool) (source : 'T seq) : 'T voption =
use enumerator = source.GetEnumerator ()
let mutable found = ValueNone

while found.IsNone && enumerator.MoveNext () do
let item = enumerator.Current

if predicate item then
found <- ValueSome item

found

/// The last element for which the predicate holds, or no value when it holds for none.
let tryFindBack (predicate : 'T -> bool) (source : 'T seq) : 'T voption = source |> Seq.toArray |> Array.tryFindBack predicate

/// The index of the first element for which the predicate holds, or no value when it holds for none.
let tryFindIndex (predicate : 'T -> bool) (source : 'T seq) : int voption =
use enumerator = source.GetEnumerator ()
let mutable index = 0
let mutable found = ValueNone

while found.IsNone && enumerator.MoveNext () do
if predicate enumerator.Current then
found <- ValueSome index

index <- index + 1

found

/// The index of the last element for which the predicate holds, or no value when it holds for none.
let tryFindIndexBack (predicate : 'T -> bool) (source : 'T seq) : int voption =
source |> Seq.toArray |> Array.tryFindIndexBack predicate

/// The first value that the chooser gives for an element, or no value when it gives none.
let tryPick (chooser : 'T -> 'U voption) (source : 'T seq) : 'U voption =
use enumerator = source.GetEnumerator ()
let mutable picked = ValueNone

while picked.IsNone && enumerator.MoveNext () do
picked <- chooser enumerator.Current

picked

/// The elements of two sequences, paired by position until the shorter one ends, as for the function of FSharp.Core.
let zip (first : 'T1 seq) (second : 'T2 seq) : struct ('T1 * 'T2) seq = Enumerable.Zip (first, second)
1 change: 1 addition & 0 deletions tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Compile Include="TestAssembly.fs" />
<Compile Include="IcedTasksProbeTests.fs" />
<Compile Include="IterationExtensionsUnitTests.fs" />
<Compile Include="ValueCollectionsTests.fs" />
<Compile Include="CosmosNameTests.fs" />
<Compile Include="ResponseMessageTests.fs" />
<Compile Include="DatabaseIdentifierTests.fs" />
Expand Down
2 changes: 1 addition & 1 deletion tests/Cosmos.Tests/ReadExtensionsTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ type ReadExtensionsIntegrationTests () =
|> Task.WhenAll

let mismatches = [|
for struct (id, expected), actual in Array.zip expectations results do
for struct (struct (id, expected), actual) in Array.zip expectations results do
if expected <> actual then
$"{id}: expected {expected}, got {actual}"
|]
Expand Down
8 changes: 8 additions & 0 deletions tests/Cosmos.Tests/TestCategories.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,11 @@ type ResponseMessageUnitTestCategoryAttribute () =
type TestInfrastructureUnitTestCategoryAttribute () =
inherit TestCategoryBaseAttribute ()
override _.TestCategories = [| "Unit"; "TestInfrastructure" |] :> IList<string>

/// <summary>
/// Categorizes <see cref="ValueCollectionsTests"/> as both a fast, emulator-free unit test and coverage for the
/// collection functions of <see cref="T:FSharp.Azure.Cosmos.ValueCollections"/>, which every project compiles.
/// </summary>
type ValueCollectionsUnitTestCategoryAttribute () =
inherit TestCategoryBaseAttribute ()
override _.TestCategories = [| "Unit"; "ValueCollections" |] :> IList<string>
Loading
Loading