Glimpse / Glimpse.Prototype

Glimpse v2 prototype
MIT License
185 stars 42 forks source link

Resource module #23

Closed nikmd23 closed 9 years ago

nikmd23 commented 9 years ago

We need to add back in the concept of resources that can serve assets and data.

In the past, Glimpse resources could:

Do we want to keep the same requirements for 2.0?

nikmd23 commented 9 years ago

High Level

Glimpse serves resources to its various clients through the resource module. Examples of resources include the client JavaScript files, JSON data and other web assets.

From a high level, resources in Glimpse 2.0 will be pieces of middleware running on a branch of the middleware chain. That branch will be split off based on a configurable baseUri, which will probably default to /glimpse.

There are two API's available:

  1. A more succinct high level API which pushes implements into "a pit of success"
  2. A more robust low level API which could be used as an "escape hatch" for advanced scenarios.

Both API's rely on Uri Template's (RFC6570) for specifying URL path & query string parameters.

Detailed Breakdown

High Level

To use the high level API, users must implement IResource

public interface IResource
{
    Task Invoke(HttpContext context, IDictionary<string, string> parameters);

    string Name { get; }

    Parameters Parameters { get; }
}

An example implementation:

public class HelloWorldResource : IResource
{
    public async Task Invoke(HttpContext context, IDictionary<string, string> parameters)
    {
        await context.Response.WriteAsync("Hello " + parameters["name"]);
    }

    public string Name => "Hello";

    public Parameters Parameters => new Parameters(Parameter.Hash, +Parameter.Custom("name"));
}

The parameters collection, which is stubbed out below, is used for a resource to signal which parameters it would like passed in. The parameters argument contains the result of parsing the URL for easy consumption.

Low Level

The low level API tracks much more closely with standard middleware, and in fact, standard middleware can be used as a resource.

The interface to be implemented is IResourceStartup:

public interface IResourceStartup
{
    void Configure(IResourceBuilder resource);
}

A sample implementation:

public class HelloWorldResource : IResourceStartup
{
    public void Configure(IResourceBuilder resource)
    {
        resource.Run("Hello", "?name={name}{&hash}", async (context, parameters) =>
        {
            await context.Response.WriteAsync("Hello " + parameters["name"]);
        });
    }
}

The low level API allows implementors to register multiple resources at once via IResourceBuilder.Run(). Implementors can also access the IApplicationBuilder via IResourceBuilder.Application.

The full IResourceBuilder is defined as follows:

public interface IResourceBuilder
{
    IApplicationBuilder AppBuilder { get; }
    //TODO: Add all Run overloads
    IResourceBuilder Run(string name, string uriTemplate, Func<HttpContext, IDictionary<string, string>, Task> resource);
}

Both implementation of HelloWorldResource would be available at /glimpse/hello/?name={name}{&hash}. (Required name, optional hash.)

The low level API allows the implementer to construct the URI template exactly how they'd like.

Other Considerations

Extension methods hanging off of context will be provided to perform common tasks such as .ConfigureCors(), .ConfigureCaching() or .AddETag(). These extension methods may be made available to IInspector's as well

Need To Implement

/// usage:
var x = new Parameters(+Parameter.Hash, -Parameter.Version, Parameter.Url, Parameter.Custom("foo", true), Parameter.Custom("bar", false));
Console.WriteLine(x.GenerateUriTemplate());

// output:
// ?hash={hash}&foo={foo}{&version,url,bar}

// implementation:
public class Parameter
{
    // Example parameters, not the final list
    public static Parameter Hash = new Parameter("hash");
    public static Parameter Url = new Parameter("url");
    public static Parameter Version = new Parameter("version");
    public static Parameter RequestId = new Parameter("requestid");
    public static Parameter Timestamp = new Parameter("stamp");
    public static Parameter Callback = new Parameter("callback");

    public Parameter(string name) : this(name, false)
    {
    }

    public Parameter(string name, bool isRequired)
    {
        Name = name;
        IsRequired = isRequired;
    }

    public static Parameter Custom(string name)
    {
            return new Parameter(name);
    }

    public static Parameter Custom(string name, bool isRequired)
    {
        return new Parameter(name, isRequired);
    }

    public string Name { get; private set; }
    public bool IsRequired { get; private set; }

    public static Parameter operator -(Parameter parameter)
    {
        return new Parameter(parameter.Name, false);
    }

    public static Parameter operator +(Parameter parameter)
    {
        return new Parameter(parameter.Name, true);
    }
}

public class Parameters : List<Parameter>
{
    public Parameters()
    {
    }

    public Parameters(params Parameter[] parameters)
    {
        foreach (var parameter in parameters)
        {
            base.Add(parameter);
        }
    }

    public new void Add(Parameter parameter)
    {
        base.Add(parameter);
    }

    public string GenerateUriTemplate()
    {
        var sb = new StringBuilder();

        var requiredParams = this.Where(p => p.IsRequired).ToArray();
        if (requiredParams.Length > 0)
        {
            sb.AppendFormat(@"?{0}={{{0}}}", requiredParams[0].Name);
            for (int i = 1; i < requiredParams.Length; i++)
            {
                sb.AppendFormat(@"&{0}={{{0}}}", requiredParams[i].Name);
            }
        }

        var optionalParams = this.Where(p => !p.IsRequired).Select(p => p.Name).ToArray();
        if (optionalParams.Length > 0)
        {
            sb.Append(string.Format("{{{1}{0}}}", string.Join(",", optionalParams), requiredParams.Length > 0 ? "&" : "?"));
        }

        return sb.ToString();
    }
}
avanderhoorn commented 9 years ago

Note, URI templates still need to be worked into the picture, but need to wait for further work to be done @darrelmiller. Will integrate as a separate PR when URI templates are ready.

darrelmiller commented 9 years ago

I was able to run existing the PCL DLL under CoreCLR (profile 259 is compatible) and I am in the process of fixing the Nuget right now. Regarding parameter matching, I should have simple string parameters for path and query string working by Monday.

avanderhoorn commented 9 years ago

Sounds great mate! As you can see from the GenerateUriTemplate method above, the default case we are providing for users is fairly simple. That said, it is only used by the higher level API and users can still define their own templates for the lower level one. So having support for the higher level one is enough for now and as you get more coverage for the other cases, we can allow the lower level one to work with more complex cases. Thanks again!

darrelmiller commented 9 years ago

I just created a prerelease Nuget of UriTemplates that has a GetParameters(Uri) method that covers the basic scenarios that I believe you need. http://www.nuget.org/packages/Tavis.UriTemplates/0.6.5-beta

avanderhoorn commented 9 years ago

I'll look at brining these in tonight. Will update with how I get on.