Skip to content

Commit

Permalink
code reordering, add trace, publicly expose build function and reduce…
Browse files Browse the repository at this point in the history
… visibility of RequireAuth property
  • Loading branch information
aspriddell committed Aug 3, 2020
1 parent 50bd93a commit 3a52f2d
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal class DatabaseUpdateRequest : ApiRequest

protected override Methods Method => Methods.Post;

protected override DataTypes DataType => DataTypes.SerializedProperty;
protected override BodyType BodyType => BodyType.SerializedProperty;

[RequestBody]
public Employee Employee { get; set; } = new Employee
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class AuthRequest : ApiRequest
{
public override string Path => "https://osu.ppy.sh/oauth/token";
protected override Methods Method => Methods.Post;
protected override DataTypes DataType => DataTypes.Encoded;
protected override BodyType BodyType => BodyType.Encoded;

[FormParameter("grant_type")]
public string Grant => "client_credentials";
Expand Down
60 changes: 27 additions & 33 deletions DragonFruit.Common.Data/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ public ApiClient(ISerializer serializer)
public string UserAgent { get; set; }

/// <summary>
/// Additional headers to be sent with the requests
/// The Authorization header value
/// </summary>
public HashableDictionary<string, string> CustomHeaders { get; set; } = new HashableDictionary<string, string>();
public string Authorization { get; set; }

/// <summary>
/// The Authorization header
/// Additional headers to be sent with the requests
/// </summary>
public string Authorization { get; set; }
public HashableDictionary<string, string> CustomHeaders { get; set; } = new HashableDictionary<string, string>();

/// <summary>
/// Optional <see cref="HttpMessageHandler"/> to be consumed by the <see cref="HttpClient"/>
Expand All @@ -75,7 +75,7 @@ public ApiClient(ISerializer serializer)
/// <remarks>
/// Defaults to <see cref="ApiJsonSerializer"/>
/// </remarks>
protected ISerializer Serializer { get; set; }
public ISerializer Serializer { get; set; }

/// <summary>
/// <see cref="HttpClient"/> used by these requests. This is used by the library and as such, should **not** be disposed in any way
Expand All @@ -87,11 +87,6 @@ public ApiClient(ISerializer serializer)
/// </summary>
protected virtual int AdjustmentTimeout => 200;

/// <summary>
/// Last <see cref="ApiRequest"/> made for using with
/// </summary>
private ApiRequest CachedRequest { get; set; }

#endregion

#region Clients, Hashes and Locks
Expand Down Expand Up @@ -136,8 +131,7 @@ protected virtual HttpClient GetClient()
//lock for modification
if (!Monitor.TryEnter(_clientAdjustmentLock, AdjustmentTimeout))
{
throw new TimeoutException(
$"The {nameof(ApiClient)} is being overloaded with reconstruction requests. Consider creating a separate {nameof(ApiClient)} and delegating clients to specific types of requests");
throw new TimeoutException($"The {nameof(ApiClient)} is being overloaded with reconstruction requests. Consider creating a separate {nameof(ApiClient)} and delegating clients to specific types of requests");
}

//wait for all ongoing requests to end
Expand Down Expand Up @@ -238,28 +232,24 @@ protected virtual void SetupRequest(HttpRequestMessage request)
public virtual HttpResponseMessage Perform(ApiRequest requestData)
{
ValidateRequest(requestData);

// perform and return postProcess result
return InternalPerform(requestData.GetRequest(Serializer), response => response, false);
return Perform(requestData.Build(this));
}

/// <summary>
/// Perform a pre-fabricated <see cref="HttpRequestMessage"/>
/// Perform an <see cref="ApiRequest"/> with a specified return type.
/// </summary>
public virtual HttpResponseMessage Perform(HttpRequestMessage request)
public virtual T Perform<T>(ApiRequest requestData) where T : class
{
return InternalPerform(request, response => response, false);
ValidateRequest(requestData);
return Perform<T>(requestData.Build(this));
}

/// <summary>
/// Perform an <see cref="ApiRequest"/> with a specified return type.
/// Perform a pre-fabricated <see cref="HttpRequestMessage"/>
/// </summary>
public virtual T Perform<T>(ApiRequest requestData) where T : class
public virtual HttpResponseMessage Perform(HttpRequestMessage request)
{
ValidateRequest(requestData);
var request = requestData.GetRequest(Serializer);

return InternalPerform(request, response => ValidateAndProcess<T>(response, request), true);
return InternalPerform(request, response => response, false);
}

/// <summary>
Expand Down Expand Up @@ -298,14 +288,19 @@ HttpResponseMessage CopyProcess(HttpResponseMessage response)
return response; //we're not using this so return anything...
}

_ = InternalPerform(requestData.GetRequest(Serializer), CopyProcess, true);
_ = InternalPerform(requestData.Build(this), CopyProcess, true);
}

/// <summary>
/// Internal procedure for performing a web-request
/// </summary>
/// <remarks>
/// While the consumer has the option to prevent disposal of the <see cref="HttpResponseMessage"/> produced,
/// the <see cref="HttpRequestMessage"/> passed is always disposed at the end of the request.
/// </remarks>
/// <param name="request">The request to perform</param>
/// <param name="processResult"><see cref="Func{T,TResult}"/> to process the <see cref="HttpResponseMessage"/></param>
/// <param name="disposeResponse">Whether to dispose of the <see cref="HttpResponseMessage"/> produced after <see cref="processResult"/> has been invoked.</param>
protected T InternalPerform<T>(HttpRequestMessage request, Func<HttpResponseMessage, T> processResult, bool disposeResponse)
{
//get client and request (disposables)
Expand Down Expand Up @@ -360,15 +355,14 @@ protected virtual T ValidateAndProcess<T>(HttpResponseMessage response, HttpRequ
/// <exception cref="ClientValidationException">The client can't be used because there is no auth url.</exception>
protected virtual void ValidateRequest(ApiRequest requestData)
{
//todo is there any benefit to trying to parse the url?
if (string.IsNullOrWhiteSpace(requestData.Path))
{
throw new NullRequestException();
}

if (requestData.RequireAuth && (!requestData.Headers.IsValueCreated && string.IsNullOrEmpty(Authorization)))
// note request path is validated on build
if (requestData.RequireAuth && string.IsNullOrEmpty(Authorization))
{
throw new ClientValidationException("Authorization data expected, but not found");
// check if we have a custom headerset in the request
if (!requestData.Headers.IsValueCreated || !requestData.Headers.Value.ContainsKey("Authorization"))
{
throw new ClientValidationException("Authorization header was expected, but not found (in request or client)");
}
}
}
}
Expand Down
51 changes: 30 additions & 21 deletions DragonFruit.Common.Data/ApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ public abstract class ApiRequest
protected virtual Methods Method => Methods.Get;

/// <summary>
/// The <see cref="DataType"/> to use (if there is a body to be sent)
/// The <see cref="BodyType"/> to use (if there is a body to be sent)
/// </summary>
protected virtual DataTypes DataType { get; }
protected virtual BodyType BodyType { get; }

/// <summary>
/// Whether an auth header is required.
/// </summary>
/// <exception cref="ClientValidationException">This was set to true but no auth header was specified.
/// Automatically suppressed if the <see cref="Headers"/> property has been initialised.
/// </exception>
public virtual bool RequireAuth => false;
protected internal virtual bool RequireAuth => false;

/// <summary>
/// Custom Headers to send with this request. Overrides any custom header set in the <see cref="HttpClient"/> with the same name.
Expand All @@ -58,10 +58,10 @@ public abstract class ApiRequest
/// Overridable property for configuring a custom body for this request
///
/// <para>
/// Only used when the <see cref="DataType"/> is equal to <see cref="DataTypes.Custom"/>
/// Only used when the <see cref="BodyType"/> is equal to <see cref="BodyType.Custom"/>
/// </para>
/// </summary>
public virtual HttpContent BodyContent { get; }
protected virtual HttpContent BodyContent { get; }

/// <summary>
/// <see cref="CultureInfo"/> used for ToString() conversions when collecting attributed members
Expand Down Expand Up @@ -106,19 +106,26 @@ internal IEnumerable<KeyValuePair<string, string>> GetParameter<T>() where T : I
}
}

internal object GetSingleParameterObject<T>() where T : Attribute
{
var property = GetType().GetProperties()
.Single(x => Attribute.GetCustomAttribute(x, typeof(T)) is T);
internal object GetSingleParameterObject<T>() where T : Attribute =>
GetType().GetProperties()
.Single(x => Attribute.GetCustomAttribute(x, typeof(T)) is T)
.GetValue(this, null);

return property.GetValue(this, null);
}
public HttpRequestMessage Build(ApiClient client) => Build(client.Serializer);

/// <summary>
/// Creates the default <see cref="HttpResponseMessage"/>, which can then be overriden by <see cref="SetupRequest"/>
/// Creates a <see cref="HttpResponseMessage"/> for this <see cref="ApiRequest"/>, which can then be modified manually or overriden by <see cref="ApiClient.SetupRequest"/>
/// </summary>
internal HttpRequestMessage GetRequest(ISerializer serializer)
/// <remarks>
/// This validates the <see cref="Path"/> and <see cref="RequireAuth"/> properties, throwing a <see cref="ClientValidationException"/> if it's unsatisfied with the constraints
/// </remarks>
public HttpRequestMessage Build(ISerializer serializer)
{
if (!Path.StartsWith("http"))
{
throw new HttpRequestException("The request path is invalid (it must start with http or https)");
}

var request = new HttpRequestMessage { RequestUri = new Uri(FullUrl) };

//generic setup
Expand All @@ -139,7 +146,7 @@ internal HttpRequestMessage GetRequest(ISerializer serializer)
break;

case Methods.Patch:
request.Method = new HttpMethod("PATCH"); //in .NET standard 2 patch isn't implemented...
request.Method = new HttpMethod("PATCH"); //in .NET Standard 2.0 patch isn't implemented...
request.Content = GetContent(serializer);
break;

Expand All @@ -152,6 +159,10 @@ internal HttpRequestMessage GetRequest(ISerializer serializer)
request.Method = HttpMethod.Head;
break;

case Methods.Trace:
request.Method = HttpMethod.Trace;
break;

default:
throw new NotImplementedException();
}
Expand All @@ -171,27 +182,25 @@ internal HttpRequestMessage GetRequest(ISerializer serializer)

private HttpContent GetContent(ISerializer serializer)
{
switch (DataType)
switch (BodyType)
{
case DataTypes.Encoded:
case BodyType.Encoded:
return new FormUrlEncodedContent(GetParameter<FormParameter>());

case DataTypes.Serialized:
case BodyType.Serialized:
return serializer.Serialize(this);

case DataTypes.SerializedProperty:
case BodyType.SerializedProperty:
var body = serializer.Serialize(GetSingleParameterObject<RequestBody>());
return body;

case DataTypes.Custom:
case BodyType.Custom:
return BodyContent;

default:
//todo custom exception - there should have been a datatype specified
throw new ArgumentOutOfRangeException();
}
}

internal ApiRequest Clone() => (ApiRequest)MemberwiseClone();
}
}
5 changes: 3 additions & 2 deletions DragonFruit.Common.Data/Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ public enum Methods
Post,
Put,
Patch,
Delete
Delete,
Trace
}

public enum DataTypes
public enum BodyType
{
/// <summary>
/// Finds all properties marked with <see cref="FormParameter"/> and creates a url-form encoded content from them
Expand Down

0 comments on commit 3a52f2d

Please sign in to comment.