Skip to content

Commit

Permalink
feat(rest): SendAsync with different request and response generics (#325
Browse files Browse the repository at this point in the history
)
  • Loading branch information
fuzzzerd authored Nov 8, 2023
1 parent 28b1ba5 commit 6cb137f
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</ItemGroup>

<PropertyGroup>
<MinVerMinimumMajorMinor>5.1</MinVerMinimumMajorMinor>
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/FMData.Rest/FMData.Rest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</ItemGroup>

<PropertyGroup>
<MinVerMinimumMajorMinor>5.1</MinVerMinimumMajorMinor>
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
Expand Down
39 changes: 13 additions & 26 deletions src/FMData.Rest/FileMakerRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,10 @@ public async Task<IResponse> LogoutAsync()
#endregion

#region Special Implementations
/// <summary>
/// General purpose Find Request method. Supports additional syntaxes like the { "omit" : "true" } operation.
/// This method returns a strongly typed <see cref="IEnumerable{T}"/> but accepts a the more flexible <see cref="Dictionary{TKey, TValue}"/> request parameters.
/// </summary>
/// <typeparam name="T">the type of response objects to return.</typeparam>
/// <param name="layout">The layout to perform the find request on.</param>
/// <param name="req">The find request dictionary.</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
/// <remarks>Can't be a relay method, since we have to process the data specially to get our output</remarks>
public override async Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionary<string, string> req)
/// <inheritdoc />
public override async Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req)
{
if (string.IsNullOrEmpty(layout)) throw new ArgumentException("Layout is required on the request.");

Expand All @@ -297,6 +291,7 @@ public override async Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionar
try
{
var responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

var responseObject = JsonConvert.DeserializeObject<FindResponse<T>>(responseJson);

return responseObject.Response.Data.Select(d => d.FieldData);
Expand Down Expand Up @@ -545,20 +540,12 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
}
}

/// <summary>
/// Strongly typed find request.
/// </summary>
/// <typeparam name="T">The type of response objects to return.</typeparam>
/// <param name="req">The find request parameters.</param>
/// <param name="fmId">Function to assign the FileMaker RecordId to each instance of {T}.</param>
/// <param name="modId">Function to assign the FileMaker ModId to each instance of {T}.</param>
/// <param name="includeDataInfo">Indicates whether the data information portion should be parsed.</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
public override async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
/// <inheritdoc />
public override async Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null)
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null)
{
if (string.IsNullOrEmpty(req.Layout)) throw new ArgumentException("Layout is required on the find request.");

Expand Down Expand Up @@ -591,7 +578,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
}

// serialize JSON results into .NET objects
IList<T> searchResults = new List<T>();
IList<TResponse> searchResults = new List<TResponse>();
foreach (var result in results)
{
var searchResult = ConvertJTokenToInstance(fmId, modId, result);
Expand Down Expand Up @@ -619,7 +606,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
if (responseObject.Messages.Any(m => m.Code == "401"))
{
// FileMaker no records match the find request => empty list.
return (new List<T>(), new DataInfoModel());
return (new List<TResponse>(), new DataInfoModel());
}
// throw FMDataException for anything not a 401.
throw new FMDataException(
Expand All @@ -631,7 +618,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
// not found, so return empty list
if (response.StatusCode == HttpStatusCode.NotFound)
{
return (new List<T>(), new DataInfoModel());
return (new List<TResponse>(), new DataInfoModel());
}

// other error
Expand Down
2 changes: 1 addition & 1 deletion src/FMData.Xml/FMData.Xml.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</ItemGroup>

<PropertyGroup>
<MinVerMinimumMajorMinor>5.1</MinVerMinimumMajorMinor>
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
Expand Down
46 changes: 16 additions & 30 deletions src/FMData.Xml/FileMakerXmlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ public override Task<IFindResponse<Dictionary<string, string>>> SendAsync(IFindR
{
throw new NotImplementedException();
}
/// <summary>
/// Finds records using a layout and a dictionary of strings as criteria.
/// </summary>
public override Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionary<string, string> req)

/// <inheritdoc />
public override Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req)
{
throw new NotImplementedException();
}
Expand Down Expand Up @@ -177,20 +178,12 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> req)
throw new Exception("Unable to complete request");
}

/// <summary>
/// Executes a Find Request and returns the matching objects projected by the type parameter.
/// </summary>
/// <typeparam name="T">The type to project the results against.</typeparam>
/// <param name="req">The Find Request Command.</param>
/// <param name="includeDataInfo">Return the data info portion of the request.</param>
/// <param name="fmId">The function to map FileMaker Record Ids to an instance of T.</param>
/// <param name="modId">The function to map FileMaker modId to an instance of T</param>
/// <returns>The projected results matching the find request.</returns>
public override async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
/// <inheritdoc />
public override async Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null)
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null)
{
var response = await ExecuteRequestAsync(req).ConfigureAwait(false);

Expand Down Expand Up @@ -225,11 +218,11 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> req)
var records = xDocument
.Descendants(_ns + "resultset")
.Elements(_ns + "record")
.Select(r => new RecordBase<T, Dictionary<string, IEnumerable<Dictionary<string, object>>>>
.Select(r => new RecordBase<TResponse, Dictionary<string, IEnumerable<Dictionary<string, object>>>>
{
RecordId = Convert.ToInt32(r.Attribute("record-id").Value),
ModId = Convert.ToInt32(r.Attribute("mod-id").Value),
FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject<T>(),
FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject<TResponse>(),
PortalData = r.Elements(_ns + "relatedset")
.ToDictionary(
k => k.Attribute("table").Value,
Expand All @@ -250,8 +243,8 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> req)
fmId?.Invoke(record.FieldData, record.RecordId);
modId?.Invoke(record.FieldData, record.ModId);

// TODO: update each record's FieldData instance with the contents of its PortalData
var portals = typeof(T).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute<PortalDataAttribute>() != null);
// update each record's FieldData instance with the contents of its PortalData
var portals = typeof(TResponse).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute<PortalDataAttribute>() != null);
foreach (var portal in portals)
{
var portalDataAttr = portal.GetCustomAttribute<PortalDataAttribute>();
Expand Down Expand Up @@ -314,7 +307,6 @@ public override Task<IResponse> SetGlobalFieldAsync(string baseTable, string fie

/// <summary>
/// Upload data to a container field.
/// TODO: Workaround with B64 encoding and container auto-enter?
/// </summary>
public override Task<IEditResponse> UpdateContainerAsync(string layout, int recordId, string fieldName, string fileName, int repetition, byte[] content)
{
Expand Down Expand Up @@ -395,19 +387,13 @@ public override Task<LayoutMetadata> GetLayoutAsync(string layout, int? recordId
throw new NotImplementedException();
}

/// <summary>
///
/// </summary>
/// <returns></returns>
/// <inheritdoc />
public override Task<IReadOnlyCollection<LayoutListItem>> GetLayoutsAsync()
{
throw new NotImplementedException();
}

/// <summary>
///
/// </summary>
/// <returns></returns>
/// <inheritdoc />
public override Task<IReadOnlyCollection<ScriptListItem>> GetScriptsAsync()
{
throw new NotImplementedException();
Expand Down
2 changes: 1 addition & 1 deletion src/FMData/FMData.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
</ItemGroup>

<PropertyGroup>
<MinVerMinimumMajorMinor>5.1</MinVerMinimumMajorMinor>
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
Expand Down
29 changes: 15 additions & 14 deletions src/FMData/FileMakerApiClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -651,28 +651,29 @@ public virtual async Task<IEnumerable<T>> SendAsync<T>(
Func<T, int, object> fmId,
Func<T, int, object> modId) where T : class, new()
{
var (data, info) = await SendAsync(req, false, fmId, modId).ConfigureAwait(false);
var (data, _) = await SendAsync<T, T>(req, false, fmId, modId).ConfigureAwait(false);
return data;
}

/// <summary>
/// Send a Find Record request to the FileMaker API.
/// </summary>
public abstract Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
/// <inheritdoc />
public virtual async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null) where T : class, new();
Func<T, int, object> modId = null) where T : class, new()
{
return await SendAsync<T, T>(req, includeDataInfo, fmId, modId).ConfigureAwait(false);
}

#endregion
/// <inheritdoc />
public abstract Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null) where TResponse : class, new();

/// <summary>
/// Find a record with utilizing a class instance to define the find request field values.
/// </summary>
/// <typeparam name="T">The response type to extract and return.</typeparam>
/// <param name="layout">The layout to perform the request on.</param>
/// <param name="req">The dictionary of key/value pairs to find against.</param>
/// <returns></returns>
#endregion
/// <inheritdoc />
public abstract Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionary<string, string> req);

/// <summary>
Expand Down
27 changes: 22 additions & 5 deletions src/FMData/IFileMakerApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="data">The initial find request data.</param>
/// <typeparam name="T">The type used for the create request.</typeparam>
/// <returns>An IFindRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IFindRequest{T} instance setup per the initial query parameter.</returns>
ICreateRequest<T> GenerateCreateRequest<T>(T data);

/// <summary>
Expand All @@ -33,7 +33,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="data">The initial edit data request.</param>
/// <typeparam name="T">The type used for the edit request.</typeparam>
/// <returns>An IEditRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IEditRequest{T} instance setup per the initial query parameter.</returns>
IEditRequest<T> GenerateEditRequest<T>(T data);

/// <summary>
Expand All @@ -46,7 +46,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="initialQuery">The initial find request data.</param>
/// <typeparam name="T">The type used for the find request.</typeparam>
/// <returns>An IFindRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IFindRequest{T} instance setup per the initial query parameter.</returns>
IFindRequest<T> GenerateFindRequest<T>(T initialQuery);

/// <summary>
Expand All @@ -56,7 +56,7 @@ public interface IFileMakerApiClient
#endregion

/// <summary>
/// Runs a script with the specified layout context and with an optional (null/empty OK) paramater.
/// Runs a script with the specified layout context and with an optional (null/empty OK) parameter.
/// </summary>
/// <param name="layout">The layout to use for the context of the script.</param>
/// <param name="script">The name of the script to run.</param>
Expand Down Expand Up @@ -353,7 +353,7 @@ public interface IFileMakerApiClient
/// <summary>
/// Delete a record by FileMaker RecordId.
/// </summary>
/// <param name="recId">The filemaker RecordId to delete.</param>
/// <param name="recId">The FileMaker RecordId to delete.</param>
/// <typeparam name="T">Used to pull the [TableAttribute] value to determine the layout to use.</typeparam>
/// <returns></returns>
/// <remarks>Use the other delete overload if the class does not use the [Table] attribute.</remarks>
Expand Down Expand Up @@ -488,6 +488,23 @@ Task<IEnumerable<T>> SendAsync<T>(
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null) where T : class, new();

/// <summary>
/// Find a record or records matching the request and include a data info model as well as the response.
/// </summary>
/// <typeparam name="TResponse">The Response type.</typeparam>
/// <typeparam name="TRequest">The Request type.</typeparam>
/// <param name="req">The find request parameters.</param>
/// <param name="fmId">Function to assign the FileMaker RecordId to each instance of {T}.</param>
/// <param name="modId">Function to assign the FileMaker ModId to each instance of {T}.</param>
/// <param name="includeDataInfo">Indicates whether the data information portion should be parsed.</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
/// <remarks>The data info portion of the response is always returned when correctly parsed.</remarks>
Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null) where TResponse : class, new();

/// <summary>
/// Edit record.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,35 @@ public async Task SendAsync_Find_Should_Have_DataInfo()
Assert.Equal(123, info.FoundCount);
}

[Fact]
public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo()
{
// arrange
var mockHttp = new MockHttpMessageHandler();

var layout = "the-layout";

mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions")
.Respond("application/json", DataApiResponses.SuccessfulAuthentication());

mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find")
.Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo());

var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection);

var toFind = new Dictionary<string, string>() { { "Id", "35" } };
var req = new FindRequest<Dictionary<string, string>>() { Layout = layout };
req.AddQuery(toFind, false);

// act
var (data, info) = await fdc.SendAsync<User, Dictionary<string, string>>(req, true);

// assert
Assert.NotEmpty(data);
Assert.Equal(1, info.ReturnedCount);
Assert.Equal(123, info.FoundCount);
}

[Fact]
public async Task SendAsyncFind_WithOmit_Omits()
{
Expand Down

0 comments on commit 6cb137f

Please sign in to comment.