Skip to content

Commit 4d7eea9

Browse files
committed
Made interface generally more FSharpy
1 parent a1ed528 commit 4d7eea9

File tree

8 files changed

+334
-230
lines changed

8 files changed

+334
-230
lines changed

README.md

Lines changed: 189 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ using TestDynamo;
4040
[Test]
4141
public async Task GetPersonById_WithValidId_ReturnsPerson()
4242
{
43-
// arrange
44-
using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>();
43+
// arrange
44+
using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>();
4545

46-
// create a table and add some items
47-
await client.CreateTableAsync(...);
48-
await client.BatchWriteItemAsync(...);
46+
// create a table and add some items
47+
await client.CreateTableAsync(...);
48+
await client.BatchWriteItemAsync(...);
4949

50-
var testSubject = new MyBeatlesService(client);
50+
var testSubject = new MyBeatlesService(client);
5151

52-
// act
53-
var beatle = testSubject.GetBeatle("Ringo");
52+
// act
53+
var beatle = testSubject.GetBeatle("Ringo");
5454

55-
// assert
56-
Assert.Equal("Starr", beatle.SecondName);
55+
// assert
56+
Assert.Equal("Starr", beatle.SecondName);
5757
}
5858
```
5959

@@ -108,8 +108,11 @@ TestDynamo is written in F# and has a lot of F# first constructs
108108
```F#
109109
open TestDynamo
110110
111+
let buildBasicClient = TestDynamoClient.createClient<AmazonDynamoDBClient> ValueNone false ValueNone false
111112
use db = new Api.FSharp.Database({ regionId = "us-west-1" })
112-
use client = ValueSome db |> TestDynamoClient.createClient ValueNone false ValueNone false
113+
use client =
114+
ValueSome db
115+
|> buildBasicClient
113116
```
114117

115118
In general, functions and extension methods with in `camelCase` are targeted at F#, where as those is `PascalCase` are targeted at C#
@@ -125,16 +128,38 @@ using var database = new Api.Database(new DatabaseId("us-west-1"));
125128

126129
// add a table
127130
database
128-
.TableBuilder("Beatles", ("FirstName", "S"))
129-
.WithGlobalSecondaryIndex("SecondNameIndex", ("SecondName", "S"), ("FirstName", "S"))
130-
.AddTable();
131+
.TableBuilder("Beatles", ("FirstName", "S"))
132+
.WithGlobalSecondaryIndex("SecondNameIndex", ("SecondName", "S"), ("FirstName", "S"))
133+
.AddTable();
131134

132135
// add some data
133136
database
134-
.ItemBuilder("Beatles")
135-
.Attribute("FirstName", "Ringo")
136-
.Attribute("SecondName", "Starr")
137-
.AddItem();
137+
.ItemBuilder("Beatles")
138+
.Attribute("FirstName", "Ringo")
139+
.Attribute("SecondName", "Starr")
140+
.AddItem();
141+
```
142+
143+
F# databases are supported also
144+
145+
```F#
146+
open TestDynamo
147+
148+
use database = new Api.FSharp.Database({ regionId = "us-west-1" });
149+
150+
// add a table
151+
database
152+
|> TableBuilder.create "Beatles" struct ("FirstName", "S") ValueNone
153+
|> TableBuilder.withGlobalSecondaryIndex "SecondNameIndex" struct ("SecondName", "S") (ValueSome struct ("FirstName", "S")) ValueNone false
154+
|> TableBuilder.addTable ValueNone
155+
156+
// add some data
157+
Map.empty
158+
|> Map.add "FirstName" (String "Ringo")
159+
|> Map.add "SecondName" (String "Starr")
160+
|> ItemBuilder.putRequest "Beatles"
161+
|> database.Put ValueNone
162+
|> ignore
138163
```
139164

140165
### Database Cloning
@@ -150,37 +175,37 @@ private static Api.Database _sharedRootDatabase = BuildDatabase();
150175

151176
private static Api.Database BuildDatabase()
152177
{
153-
var database = new Api.Database(new DatabaseId("us-west-1"));
154-
155-
// add a table
156-
database
157-
.TableBuilder("Beatles", ("FirstName", "S"))
158-
.WithGlobalSecondaryIndex("SecondNameIndex", ("SecondName", "S"), ("FirstName", "S"))
159-
.AddTable();
160-
161-
// add some data
162-
database
163-
.ItemBuilder("Beatles")
164-
.Attribute("FirstName", "Ringo")
165-
.Attribute("SecondName", "Starr")
166-
.AddItem();
167-
168-
return database;
178+
var database = new Api.Database(new DatabaseId("us-west-1"));
179+
180+
// add a table
181+
database
182+
.TableBuilder("Beatles", ("FirstName", "S"))
183+
.WithGlobalSecondaryIndex("SecondNameIndex", ("SecondName", "S"), ("FirstName", "S"))
184+
.AddTable();
185+
186+
// add some data
187+
database
188+
.ItemBuilder("Beatles")
189+
.Attribute("FirstName", "Ringo")
190+
.Attribute("SecondName", "Starr")
191+
.AddItem();
192+
193+
return database;
169194
}
170195

171196
[Test]
172197
public async Task TestSomething()
173198
{
174-
// clone the database to get working copy
175-
// without altering the original
176-
using var database = _sharedRootDatabase.Clone();
177-
using var client = database.CreateClient<AmazonDynamoDBClient>();
199+
// clone the database to get working copy
200+
// without altering the original
201+
using var database = _sharedRootDatabase.Clone();
202+
using var client = database.CreateClient<AmazonDynamoDBClient>();
178203

179-
// act
180-
...
204+
// act
205+
...
181206

182-
// assert
183-
...
207+
// assert
208+
...
184209
}
185210
```
186211

@@ -203,6 +228,22 @@ var ringo = database
203228
.Single(v => v["FirstName"].S == "Ringo");
204229
```
205230

231+
Or with F#
232+
233+
```F#
234+
use database = GetMeADatabase()
235+
236+
let ringo =
237+
database.GetTable ValueNone "Beatles"
238+
|> LazyDebugTable.getValues ValueNone
239+
|> Seq.filter (
240+
_.InternalItem
241+
>> Item.attributes
242+
>> Map.find "FirstName"
243+
>> (=) (String "Ringo"))
244+
|> Seq.head
245+
```
246+
206247
### Streaming and Subscriptions
207248

208249
If streams are enabled on tables they can be used for global table
@@ -219,19 +260,55 @@ using TestDynamo.Lambda;
219260
using Amazon.Lambda.DynamoDBEvents;
220261

221262
var subscription = database.AddSubscription<DynamoDBEvent>(
222-
"Beatles",
223-
(dynamoDbStreamsEvent, cancellationToken) =>
224-
{
225-
var added = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.NewImage?["FirstName"]?.S;
226-
if (added != null)
227-
Console.WriteLine($"{added} has joined the Beatles");
263+
"Beatles",
264+
(dynamoDbStreamsEvent, cancellationToken) =>
265+
{
266+
var added = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.NewImage?["FirstName"]?.S;
267+
if (added != null)
268+
Console.WriteLine($"{added} has joined the Beatles");
228269

229-
var removed = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.OldImage?["FirstName"]?.S;
230-
if (removed != null)
231-
Console.WriteLine($"{removed} has left the Beatles");
270+
var removed = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.OldImage?["FirstName"]?.S;
271+
if (removed != null)
272+
Console.WriteLine($"{removed} has left the Beatles");
232273

233-
return default;
234-
});
274+
return default;
275+
});
276+
277+
// disposing will remove the subscription
278+
subscription.Dispose();
279+
```
280+
281+
Or with F#
282+
283+
```F#
284+
open TestDynamo
285+
open TestDynamo.Lambda
286+
open Amazon.Lambda.DynamoDBEvents
287+
288+
let subscriber (dynamoDbStreamsEvent: DynamoDBEvent) _ =
289+
290+
let tryFirst f =
291+
dynamoDbStreamsEvent.Records
292+
|> Seq.map f
293+
|> Seq.filter ((<>) null)
294+
|> Seq.map (fun (x: Dictionary<string, AttributeValue>) -> x["FirstName"].S)
295+
|> Seq.tryHead
296+
297+
match tryFirst _.Dynamodb.NewImage with
298+
| Some x -> printf "%s has joined the Beatles" x
299+
| None -> ()
300+
301+
match tryFirst _.Dynamodb.OldImage with
302+
| Some x -> printf "%s has left the Beatles" x
303+
| None -> ()
304+
305+
Unchecked.defaultOf<_>
306+
307+
let subscription =
308+
Subscriptions.addSubscription
309+
(SubscriptionDetails.ofTableName "Beatles")
310+
subscriber
311+
database
235312
236313
// disposing will remove the subscription
237314
subscription.Dispose();
@@ -241,28 +318,28 @@ Subscribe to raw changes
241318

242319
```C#
243320
var subscription = database
244-
.SubscribeToStream("Beatles", (cdcPacket, cancellationToken) =>
245-
{
246-
var added = cdcPacket.data.packet.changeResult.OrderedChanges
247-
.Select(x => x.Put)
248-
.Where(x => x.IsSome)
249-
.Select(x => x.Value["FirstName"].S)
250-
.FirstOrDefault();
321+
.SubscribeToStream("Beatles", (cdcPacket, cancellationToken) =>
322+
{
323+
var added = cdcPacket.data.packet.changeResult.OrderedChanges
324+
.Select(x => x.Put)
325+
.Where(x => x.IsSome)
326+
.Select(x => x.Value["FirstName"].S)
327+
.FirstOrDefault();
251328

252-
if (added != null)
253-
Console.WriteLine($"{added} has joined the Beatles");
329+
if (added != null)
330+
Console.WriteLine($"{added} has joined the Beatles");
254331

255-
var removed = cdcPacket.data.packet.changeResult.OrderedChanges
256-
.Select(x => x.Deleted)
257-
.Where(x => x.IsSome)
258-
.Select(x => x.Value["FirstName"].S)
259-
.FirstOrDefault();
332+
var removed = cdcPacket.data.packet.changeResult.OrderedChanges
333+
.Select(x => x.Deleted)
334+
.Where(x => x.IsSome)
335+
.Select(x => x.Value["FirstName"].S)
336+
.FirstOrDefault();
260337

261-
if (removed != null)
262-
Console.WriteLine($"{removed} has left the Beatles");
338+
if (removed != null)
339+
Console.WriteLine($"{removed} has left the Beatles");
263340

264-
return default;
265-
});
341+
return default;
342+
});
266343

267344
// disposing will remove the subscription
268345
subscription.Dispose();
@@ -298,8 +375,8 @@ using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>();
298375
using var context = new DynamoDbContext(client)
299376
await context.SaveAsync(new Beatle
300377
{
301-
FirstName = "Ringo",
302-
SecondName = "Starr"
378+
FirstName = "Ringo",
379+
SecondName = "Starr"
303380
})
304381
```
305382

@@ -429,6 +506,23 @@ using var db2 = DatabaseSerializer.Database.FromFile(@"TestData.json");
429506
var json = DatabaseSerializer.GlobalDatabase.ToString(globalDb);
430507
```
431508

509+
Or F#
510+
511+
```F#
512+
open TestDynamo
513+
open TestDynamo.Serialization
514+
515+
use db1 = new Api.FSharp.Database()
516+
... populate database
517+
518+
DatabaseSerializer.FSharp.Database.ToFile(db1, @"TestData.json")
519+
520+
use db2 = DatabaseSerializer.FSharp.Database.FromFile(@"TestData.json")
521+
522+
// there are also tools to serialize and deserialze global databases
523+
let json = DatabaseSerializer.FSharp.GlobalDatabase.ToString(globalDb)
524+
```
525+
432526
Serialization is designed to share data between test runs, but ultimately, it scales with the number of items in the database. This means
433527
that it may take more time than is ideal for executing fast unit tests. [Database cloning](#database-cloning) is a better solution for large databases which are shared between multiple tests, as it executes instantly for any sized database or global database
434528

@@ -448,6 +542,22 @@ using var database = await CloudFormationParser.BuildDatabase(new[] { cfnFile1,
448542
...
449543
```
450544

545+
Or with F#
546+
547+
```F#
548+
open TestDynamo.Serialization;
549+
550+
async {
551+
use! database =
552+
[ { region = "eu-north-1"
553+
fileJson = File.ReadAllText("myTemplate1.json") }
554+
{ region = "us-west-2"
555+
fileJson = File.ReadAllText("myTemplate2.json") } ]
556+
|> CloudFormationParser.buildDatabase { ignoreUnsupportedResources = true } ValueNone
557+
...
558+
}
559+
```
560+
451561
### Locking and Atomic transactions
452562

453563
Test dynamo is more consistant than DynamoDb. In general, all operations on a single database (region) are atomic.
@@ -478,12 +588,12 @@ using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>(recordCal
478588
await client.CreateTableAsync(...);
479589
try
480590
{
481-
// failed request to put an item
482-
await client.PutItemAsync(...);
591+
// failed request to put an item
592+
await client.PutItemAsync(...);
483593
}
484594
catch
485595
{
486-
// do nothing
596+
// do nothing
487597
}
488598

489599

@@ -597,20 +707,20 @@ using var database = new Api.Database(new DatabaseId("us-west-1"));
597707
var interceptor = new CreateBackupInterceptor(backups);
598708
using var client = database.CreateClient<AmazonDynamoDBClient>(interceptor);
599709

600-
// execute some requests which are not intercepted. These will not be intercepted
710+
// execute some requests which are not intercepted
601711
await client.PutItemAsync(...);
602712
await client.PutItemAsync(...);
603713

604714
// create a backup. This will be intercepted
605715
var backupResponse = await client.CreateBackupAsync(new CreateBackupRequest
606716
{
607-
TableName = "Beatles"
717+
TableName = "Beatles"
608718
});
609719

610720
// restore from backup. This will be intercepted
611721
await client.RestoreTableFromBackupAsync(new RestoreTableFromBackupRequest
612722
{
613-
BackupArn = backupResponse.BackupDetails.BackupArn
723+
BackupArn = backupResponse.BackupDetails.BackupArn
614724
});
615725
```
616726

0 commit comments

Comments
 (0)