456 lines
19 KiB
C#
456 lines
19 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Configuration;
|
||
|
using System.IO;
|
||
|
using System.Linq;
|
||
|
using System.Reflection;
|
||
|
using System.Runtime.Serialization;
|
||
|
using System.Text.RegularExpressions;
|
||
|
using System.Web;
|
||
|
using System.Web.Routing;
|
||
|
using System.Xml.Linq;
|
||
|
using ASC.Api.Enums;
|
||
|
using ASC.Api.Impl;
|
||
|
using ASC.Api.Interfaces;
|
||
|
using ASC.Api.Utils;
|
||
|
using Autofac;
|
||
|
using Autofac.Core;
|
||
|
using log4net;
|
||
|
|
||
|
[DataContract(Name = "response", Namespace = "")]
|
||
|
public class MsDocFunctionResponse
|
||
|
{
|
||
|
[DataMember(Name = "output")]
|
||
|
public Dictionary<string, string> Outputs { get; set; }
|
||
|
}
|
||
|
|
||
|
[DataContract(Name = "entrypoint", Namespace = "")]
|
||
|
public class MsDocEntryPoint
|
||
|
{
|
||
|
[DataMember(Name = "summary")]
|
||
|
public string Summary { get; set; }
|
||
|
|
||
|
[DataMember(Name = "remarks")]
|
||
|
public string Remarks { get; set; }
|
||
|
|
||
|
[DataMember(Name = "name")]
|
||
|
public string Name { get; set; }
|
||
|
|
||
|
[DataMember(Name = "example")]
|
||
|
public string Example { get; set; }
|
||
|
|
||
|
[DataMember(Name = "functions")]
|
||
|
public List<MsDocEntryPointMethod> Methods { get; set; }
|
||
|
}
|
||
|
|
||
|
[DataContract(Name = "function", Namespace = "")]
|
||
|
public class MsDocEntryPointMethod : IEquatable<MsDocEntryPointMethod>
|
||
|
{
|
||
|
[DataMember(Name = "summary")]
|
||
|
public string Summary { get; set; }
|
||
|
|
||
|
[DataMember(Name = "authentification", EmitDefaultValue = true)]
|
||
|
public bool Authentification { get; set; }
|
||
|
|
||
|
[DataMember(Name = "remarks")]
|
||
|
public string Remarks { get; set; }
|
||
|
|
||
|
[DataMember(Name = "httpmethod")]
|
||
|
public string HttpMethod { get; set; }
|
||
|
|
||
|
[DataMember(Name = "url")]
|
||
|
public string Path { get; set; }
|
||
|
|
||
|
[DataMember(Name = "example")]
|
||
|
public string Example { get; set; }
|
||
|
|
||
|
[DataMember(Name = "params")]
|
||
|
public List<MsDocEntryPointMethodParams> Params { get; set; }
|
||
|
|
||
|
[DataMember(Name = "returns")]
|
||
|
public string Returns { get; set; }
|
||
|
|
||
|
[DataMember(Name = "responses")]
|
||
|
public List<MsDocFunctionResponse> Response { get; set; }
|
||
|
|
||
|
[DataMember(Name = "category")]
|
||
|
public string Category { get; set; }
|
||
|
|
||
|
[DataMember(Name = "notes")]
|
||
|
public string Notes { get; set; }
|
||
|
|
||
|
[DataMember(Name = "short")]
|
||
|
public string ShortName { get; set; }
|
||
|
|
||
|
[DataMember(Name = "function")]
|
||
|
public string FunctionName { get; set; }
|
||
|
|
||
|
public MsDocEntryPoint Parent { get; set; }
|
||
|
|
||
|
[DataMember(Name = "visible")]
|
||
|
public bool Visible { get; set; }
|
||
|
|
||
|
public override bool Equals(object obj)
|
||
|
{
|
||
|
if (ReferenceEquals(null, obj)) return false;
|
||
|
if (ReferenceEquals(this, obj)) return true;
|
||
|
if (obj.GetType() != typeof(MsDocEntryPointMethod)) return false;
|
||
|
return Equals((MsDocEntryPointMethod)obj);
|
||
|
}
|
||
|
|
||
|
public bool Equals(MsDocEntryPointMethod other)
|
||
|
{
|
||
|
if (ReferenceEquals(null, other)) return false;
|
||
|
if (ReferenceEquals(this, other)) return true;
|
||
|
return Equals(other.Path, Path) && Equals(other.HttpMethod, HttpMethod);
|
||
|
}
|
||
|
|
||
|
public override string ToString()
|
||
|
{
|
||
|
return string.Format("{0} {1}", HttpMethod, Path).ToLowerInvariant();
|
||
|
}
|
||
|
|
||
|
public override int GetHashCode()
|
||
|
{
|
||
|
unchecked
|
||
|
{
|
||
|
return ((Path != null ? Path.GetHashCode() : 0) * 397) ^ (HttpMethod != null ? HttpMethod.GetHashCode() : 0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static bool operator ==(MsDocEntryPointMethod left, MsDocEntryPointMethod right)
|
||
|
{
|
||
|
return Equals(left, right);
|
||
|
}
|
||
|
|
||
|
public static bool operator !=(MsDocEntryPointMethod left, MsDocEntryPointMethod right)
|
||
|
{
|
||
|
return !Equals(left, right);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[DataContract(Name = "param", Namespace = "")]
|
||
|
public class MsDocEntryPointMethodParams
|
||
|
{
|
||
|
[DataMember(Name = "name")]
|
||
|
public string Name { get; set; }
|
||
|
|
||
|
[DataMember(Name = "type")]
|
||
|
public string Type { get; set; }
|
||
|
|
||
|
[DataMember(Name = "sendmethod")]
|
||
|
public string Method { get; set; }
|
||
|
|
||
|
[DataMember(Name = "description")]
|
||
|
public string Description { get; set; }
|
||
|
|
||
|
[DataMember(Name = "remarks")]
|
||
|
public string Remarks { get; set; }
|
||
|
|
||
|
[DataMember(Name = "optional")]
|
||
|
public bool IsOptional { get; set; }
|
||
|
|
||
|
[DataMember(Name = "visible")]
|
||
|
public bool Visible { get; set; }
|
||
|
}
|
||
|
|
||
|
public class MsDocDocumentGenerator
|
||
|
{
|
||
|
private static readonly Regex RouteRegex = new Regex(@"\{([^\}]+)\}", RegexOptions.Compiled);
|
||
|
private readonly List<MsDocEntryPoint> _points = new List<MsDocEntryPoint>();
|
||
|
private readonly string[] _responseFormats = (ConfigurationManager.AppSettings["enabled_response_formats"] ?? "").Split('|');
|
||
|
|
||
|
public MsDocDocumentGenerator(IContainer container)
|
||
|
{
|
||
|
Container = container;
|
||
|
}
|
||
|
|
||
|
#region IApiDocumentGenerator Members
|
||
|
|
||
|
public IContainer Container { get; set; }
|
||
|
|
||
|
public List<MsDocEntryPoint> Points
|
||
|
{
|
||
|
get { return _points; }
|
||
|
}
|
||
|
|
||
|
public void GenerateDocForEntryPoint(IComponentRegistration apiEntryPointRegistration, IEnumerable<IApiMethodCall> apiMethodCalls)
|
||
|
{
|
||
|
//Find the document
|
||
|
var lookupDir = AppDomain.CurrentDomain.RelativeSearchPath;
|
||
|
var docFile = Path.Combine(lookupDir, Path.GetFileName(apiEntryPointRegistration.Activator.LimitType.Assembly.Location).ToLowerInvariant().Replace(".dll", ".xml"));
|
||
|
|
||
|
if (!File.Exists(docFile))
|
||
|
{
|
||
|
//Build without doc
|
||
|
BuildUndocumented(apiEntryPointRegistration, apiMethodCalls);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var members = XDocument.Load(docFile).Root.ThrowIfNull(new ArgumentException("Bad documentation file " + docFile)).Element("members").Elements("member");
|
||
|
//Find entry point first
|
||
|
var entryPointDoc = members.SingleOrDefault(x => x.Attribute("name").ValueOrNull() == string.Format("T:{0}", apiEntryPointRegistration.Activator.LimitType.FullName))
|
||
|
?? new XElement("member",
|
||
|
new XElement("summary", "This entry point doesn't have documentation."),
|
||
|
new XElement("remarks", ""));
|
||
|
|
||
|
var methodCallsDoc = from apiMethodCall in apiMethodCalls
|
||
|
let memberdesc = (from member in members
|
||
|
where member.Attribute("name").ValueOrNull() == GetMethodString(apiMethodCall.MethodCall)
|
||
|
select member).SingleOrDefault()
|
||
|
select new { apiMethod = apiMethodCall, description = memberdesc ?? CreateEmptyParams(apiMethodCall) };
|
||
|
|
||
|
//Ughh. we got all what we need now building
|
||
|
var root = new MsDocEntryPoint
|
||
|
{
|
||
|
Summary = entryPointDoc.Element("summary").ValueOrNull(),
|
||
|
Remarks = entryPointDoc.Element("remarks").ValueOrNull(),
|
||
|
Name = apiEntryPointRegistration.Services.OfType<KeyedService>().First().ServiceKey.ToString(),
|
||
|
Example = entryPointDoc.Element("example").ValueOrNull(),
|
||
|
|
||
|
Methods = (from methodCall in methodCallsDoc
|
||
|
let pointMethod = new MsDocEntryPointMethod
|
||
|
{
|
||
|
Path = methodCall.apiMethod.FullPath,
|
||
|
HttpMethod = methodCall.apiMethod.HttpMethod,
|
||
|
Authentification = methodCall.apiMethod.RequiresAuthorization,
|
||
|
FunctionName = GetFunctionName(methodCall.apiMethod.MethodCall.Name),
|
||
|
Summary = methodCall.description.Element("summary").ValueOrNull(),
|
||
|
Visible = !string.Equals(methodCall.description.Element("visible").ValueOrNull(), bool.FalseString, StringComparison.OrdinalIgnoreCase),
|
||
|
Remarks = methodCall.description.Element("remarks").ValueOrNull().Replace(Environment.NewLine, @"<br />"),
|
||
|
Returns = methodCall.description.Element("returns").ValueOrNull(),
|
||
|
Example = methodCall.description.Element("example").ValueOrNull().Replace(Environment.NewLine, @"<br />"),
|
||
|
Response = TryCreateResponse(methodCall.apiMethod, Container, methodCall.description.Element("returns")),
|
||
|
Category = methodCall.description.Element("category").ValueOrNull(),
|
||
|
Notes = methodCall.description.Element("notes").ValueOrNull(),
|
||
|
ShortName = methodCall.description.Element("short").ValueOrNull(),
|
||
|
Params = (from methodParam in methodCall.description.Elements("param")
|
||
|
select new MsDocEntryPointMethodParams
|
||
|
{
|
||
|
Description = methodParam.ValueOrNull(),
|
||
|
Name = methodParam.Attribute("name").ValueOrNull(),
|
||
|
Remarks = methodParam.Attribute("remark").ValueOrNull(),
|
||
|
IsOptional = string.Equals(methodParam.Attribute("optional").ValueOrNull(), bool.TrueString, StringComparison.OrdinalIgnoreCase),
|
||
|
Visible = !string.Equals(methodParam.Attribute("visible").ValueOrNull(), bool.FalseString, StringComparison.OrdinalIgnoreCase),
|
||
|
Type = methodCall.apiMethod.GetParams().Where(x => x.Name == methodParam.Attribute("name").ValueOrNull()).Select(x => GetParameterTypeRepresentation(x.ParameterType)).SingleOrDefault(),
|
||
|
Method = GuesMethod(methodParam.Attribute("name").ValueOrNull(), methodCall.apiMethod.RoutingUrl, methodCall.apiMethod.HttpMethod)
|
||
|
}).ToList()
|
||
|
}
|
||
|
where pointMethod.Visible
|
||
|
select pointMethod).ToList()
|
||
|
};
|
||
|
Points.Add(root);
|
||
|
}
|
||
|
|
||
|
private static string GetParameterTypeRepresentation(Type paramType)
|
||
|
{
|
||
|
return paramType.IsEnum
|
||
|
? string.Join(", ", Enum.GetNames(paramType))
|
||
|
: paramType.ToString();
|
||
|
}
|
||
|
|
||
|
private void BuildUndocumented(IComponentRegistration apiEntryPointRegistration, IEnumerable<IApiMethodCall> apiMethodCalls)
|
||
|
{
|
||
|
var root = new MsDocEntryPoint
|
||
|
{
|
||
|
Name = apiEntryPointRegistration.Services.OfType<KeyedService>().First().ServiceKey.ToString(),
|
||
|
Remarks = "This entry point doesn't have any documentation. This is generated automaticaly using metadata",
|
||
|
Methods = (from methodCall in apiMethodCalls
|
||
|
select new MsDocEntryPointMethod
|
||
|
{
|
||
|
Path = methodCall.FullPath,
|
||
|
HttpMethod = methodCall.HttpMethod,
|
||
|
FunctionName = GetFunctionName(methodCall.MethodCall.Name),
|
||
|
Authentification = methodCall.RequiresAuthorization,
|
||
|
Response = TryCreateResponse(methodCall, Container, null),
|
||
|
Params = (from methodParam in
|
||
|
methodCall.GetParams()
|
||
|
select new MsDocEntryPointMethodParams
|
||
|
{
|
||
|
Name = methodParam.Name,
|
||
|
Visible = true,
|
||
|
Type = GetParameterTypeRepresentation(methodParam.ParameterType),
|
||
|
Method =
|
||
|
GuesMethod(
|
||
|
methodParam.Name,
|
||
|
methodCall.RoutingUrl, methodCall.HttpMethod)
|
||
|
}
|
||
|
).ToList()
|
||
|
}
|
||
|
).ToList()
|
||
|
};
|
||
|
Points.Add(root);
|
||
|
}
|
||
|
|
||
|
private static string GetFunctionName(string functionName)
|
||
|
{
|
||
|
return Regex.Replace(Regex.Replace(functionName, "[a-z][A-Z]+", match => (match.Value[0] + " " + match.Value.Substring(1, match.Value.Length - 2) + (" " + match.Value[match.Value.Length - 1]).ToLowerInvariant())), @"\s+", " ");
|
||
|
}
|
||
|
|
||
|
private static XElement CreateEmptyParams(IApiMethodCall apiMethodCall)
|
||
|
{
|
||
|
return new XElement("description", apiMethodCall.GetParams().Select(x => new XElement("param", new XAttribute("name", x.Name))));
|
||
|
}
|
||
|
|
||
|
private readonly HashSet<Type> _alreadyRegisteredTypes = new HashSet<Type>();
|
||
|
|
||
|
private List<MsDocFunctionResponse> TryCreateResponse(IApiMethodCall apiMethod, IContainer container, XElement returns)
|
||
|
{
|
||
|
var samples = new List<MsDocFunctionResponse>();
|
||
|
var returnType = apiMethod.MethodCall.ReturnType;
|
||
|
var collection = false;
|
||
|
if (apiMethod.MethodCall.ReturnType.IsGenericType)
|
||
|
{
|
||
|
//It's collection
|
||
|
returnType = apiMethod.MethodCall.ReturnType.GetGenericArguments().FirstOrDefault();
|
||
|
collection = true;
|
||
|
}
|
||
|
if (returnType != null)
|
||
|
{
|
||
|
var sample = returnType.GetMethod("GetSample", BindingFlags.Static | BindingFlags.Public);
|
||
|
if (sample != null)
|
||
|
{
|
||
|
samples = GetSamples(apiMethod, container, sample, collection, returnType).ToList();
|
||
|
_alreadyRegisteredTypes.Add(returnType);
|
||
|
}
|
||
|
else if (returns != null && returns.Elements("see").Any())
|
||
|
{
|
||
|
var cref = returns.Elements("see").Attributes("cref").FirstOrDefault().ValueOrNull();
|
||
|
if (!string.IsNullOrEmpty(cref) && cref.StartsWith("T:"))
|
||
|
{
|
||
|
var classname = cref.Substring(2);
|
||
|
//Get the type
|
||
|
try
|
||
|
{
|
||
|
|
||
|
var crefType = Type.GetType(classname) ??
|
||
|
_alreadyRegisteredTypes.SingleOrDefault(x => x.Name.Equals(classname, StringComparison.OrdinalIgnoreCase)) ??
|
||
|
apiMethod.MethodCall.Module.GetType(classname, true) ??
|
||
|
apiMethod.MethodCall.Module.GetTypes().SingleOrDefault(x => x.Name.Equals(classname, StringComparison.OrdinalIgnoreCase)) ??
|
||
|
AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes()).SingleOrDefault(x => x.Name.Equals(classname, StringComparison.OrdinalIgnoreCase));
|
||
|
|
||
|
if (crefType != null)
|
||
|
{
|
||
|
sample = crefType.GetMethod("GetSample", BindingFlags.Static | BindingFlags.Public);
|
||
|
if (sample != null)
|
||
|
{
|
||
|
samples = GetSamples(apiMethod, container, sample, collection, crefType).ToList();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (Exception)
|
||
|
{
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return samples;
|
||
|
}
|
||
|
|
||
|
private IEnumerable<MsDocFunctionResponse> GetSamples(IApiMethodCall apiMethod, IContainer container, MethodInfo sample, bool collection, Type returnType)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
using (var lifetimeScope = container.BeginLifetimeScope())
|
||
|
{
|
||
|
var routeContext = new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData());
|
||
|
var apiContext = lifetimeScope.Resolve<ApiContext>(new NamedParameter("requestContext", routeContext));
|
||
|
var response = lifetimeScope.Resolve<IApiStandartResponce>();
|
||
|
response.Status = ApiStatus.Ok;
|
||
|
|
||
|
apiContext.RegisterType(returnType);
|
||
|
|
||
|
var sampleResponse = sample.Invoke(null, new object[0]);
|
||
|
if (collection)
|
||
|
{
|
||
|
//wrap in array
|
||
|
sampleResponse = new List<object> { sampleResponse };
|
||
|
}
|
||
|
response.Response = sampleResponse;
|
||
|
|
||
|
var serializers = container.Resolve<IEnumerable<IApiSerializer>>().Where(x => x.CanSerializeType(apiMethod.MethodCall.ReturnType));
|
||
|
return serializers.Select(apiResponder => new MsDocFunctionResponse
|
||
|
{
|
||
|
Outputs = CreateResponse(apiResponder, response, apiContext)
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
catch (Exception err)
|
||
|
{
|
||
|
LogManager.GetLogger("ASC.Api").Error(err);
|
||
|
return Enumerable.Empty<MsDocFunctionResponse>();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private Dictionary<string, string> CreateResponse(IApiSerializer apiResponder, IApiStandartResponce response, ApiContext apiContext)
|
||
|
{
|
||
|
var examples = new Dictionary<string, string>();
|
||
|
foreach (var extension in apiResponder.GetSupportedExtensions().Where(extension => _responseFormats.Contains(extension)))
|
||
|
{
|
||
|
//Create request context
|
||
|
using (var writer = new StringWriter())
|
||
|
{
|
||
|
var contentType = apiResponder.RespondTo(response, writer, "dummy" + extension, string.Empty, true, false);
|
||
|
writer.Flush();
|
||
|
examples[contentType.MediaType] = writer.GetStringBuilder().ToString();
|
||
|
}
|
||
|
}
|
||
|
return examples;
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
private static string GuesMethod(string textAttr, string routingUrl, string httpmethod)
|
||
|
{
|
||
|
if ("get".Equals(httpmethod, StringComparison.OrdinalIgnoreCase))
|
||
|
return "url";
|
||
|
var matches = RouteRegex.Matches(routingUrl);
|
||
|
textAttr = textAttr.ToLowerInvariant();
|
||
|
return
|
||
|
matches.Cast<Match>().Where(x => x.Success && x.Groups[1].Success).Select(
|
||
|
x => x.Groups[1].Value.ToLowerInvariant()).Any(
|
||
|
routeConstr => routeConstr == textAttr || routeConstr.StartsWith(textAttr + ":"))
|
||
|
? "url"
|
||
|
: "body";
|
||
|
}
|
||
|
|
||
|
public static string GetMethodString(MethodBase methodCall)
|
||
|
{
|
||
|
var str = string.Format("M:{0}.{1}", methodCall.DeclaringType.FullName, methodCall.Name);
|
||
|
var callParam = methodCall.GetParameters();
|
||
|
if (callParam.Length > 0)
|
||
|
{
|
||
|
str += string.Format("({0})",
|
||
|
string.Join(",", callParam.Select(x => MakeParamName(x.ParameterType)).ToArray()));
|
||
|
}
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
private static string MakeParamName(Type parameterType)
|
||
|
{
|
||
|
var name = parameterType.FullName.Replace('+', '.');
|
||
|
name = Regex.Replace(name, @"\[.+\]", "");
|
||
|
if (parameterType.IsGenericType)
|
||
|
{
|
||
|
name = Regex.Replace(name, @"`\d+", "");
|
||
|
var genericTypes = parameterType.GetGenericArguments();
|
||
|
name += "{" + string.Join(",", genericTypes.Select(MakeParamName).ToArray()) + "}";
|
||
|
}
|
||
|
return name;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
internal static class XmlExt
|
||
|
{
|
||
|
internal static string ValueOrNull(this XElement element)
|
||
|
{
|
||
|
return element == null ? "" : element.Value;
|
||
|
}
|
||
|
|
||
|
internal static string ValueOrNull(this XAttribute element)
|
||
|
{
|
||
|
return element == null ? "" : element.Value;
|
||
|
}
|
||
|
}
|