protected virtual ParsedApiDesc ParseAction(ApiControllerDesc controller, ApiActionDesc action) { var actionRegex = @"(?:\[action\]|\{action\})"; var controllerRegex = @"(?:\[controller\]|\{controller\})"; string template = _options.DefaultRouteTemplate; if (controller.RouteTemplate != null || action.RouteTemplate != null) { if (controller.RouteTemplate != null) { template = controller.RouteTemplate; } if (action.RouteTemplate != null) { var art = action.RouteTemplate.TrimStart('~'); var absolute = art.StartsWith("/"); template = absolute ? art : JoinUrls(template, art); } template = template.Trim('/'); } template = Regex.Replace(template, actionRegex, action.ActionName, RegexOptions.IgnoreCase); template = Regex.Replace(template, controllerRegex, controller.ControllerName, RegexOptions.IgnoreCase); ApiParamDesc postParameter = null; ApiParamDesc modelParameter = null; ApiParamDesc[] getParameters = ValidateParameters(action.Parameters, action.Method, template, out postParameter, out modelParameter); string GetParamString(IEnumerable <ApiParamDesc> aa) => String.Join(", ", aa.Select(p => $"{p.ParameterName}{(p.IsOptional ? "?" : "")}: {_converter.GetTypeScriptName(p.ParameterType)}")); var path = BuildUrlString(action, template, getParameters, modelParameter); return(new ParsedApiDesc { EndpointParamString = GetParamString(modelParameter != null ? new[] { modelParameter } : getParameters), ParamString = GetParamString(action.Parameters), PathString = path, PostParameter = postParameter, ModelParameter = modelParameter, GetParameters = getParameters, NameString = _options.FnActionName(action), Action = action, Template = template, }); }
protected virtual string BuildUrlString(ApiActionDesc action, string template, ApiParamDesc[] getParameters, ApiParamDesc modelParameter) { List <string> routeJs = new List <string>(); List <string> queryJs = new List <string>(); List <ApiParamDesc> routeParameters = new List <ApiParamDesc>(); bool seenOptionalRoute = false; string GetParamExecString(ApiParamDesc p) { var name = modelParameter == null ? p.ParameterName : $"{modelParameter.ParameterName}.{p.ParameterName}"; var imm = CreateTypeInitializerMethod(p.ParameterType); if (imm != null) { return($"from_{imm}({name})"); } return(name); } var parts = template.Split('/'); for (int i = 0; i < parts.Length; i++) { var part = parts[i]; if (!part.StartsWith("{")) { routeJs.Add("\"" + part + "\""); continue; } part = part.Substring(1, part.Length - 2); var typeIndex = part.IndexOf(":"); if (typeIndex > 0) { part = part.Substring(0, typeIndex); } if (part.StartsWith("*")) { part = part.Substring(1); } var templateOptional = part.EndsWith("?"); part = part.TrimEnd('?'); if (templateOptional) { seenOptionalRoute = true; } else if (seenOptionalRoute == true) { throw new Exception($"In action '{action.ActionName}', required route parameter must not come after an optional route parameter in template: '{template}'."); } var get = getParameters.FirstOrDefault(g => g.ParameterName.Equals(part)); if (get == null) { throw new Exception($"In action '{action.ActionName}', route parameter `{part}` does not match any available method parameters. " + $"Please check your route template: '{template}'." + $"Parameters: [{String.Join(", ", action.Parameters.Select(s => s.ParameterName))}]"); } if (templateOptional != get.IsOptional) { bool canBeNull = !get.ParameterType.GetDnxCompatible().IsValueType || (Nullable.GetUnderlyingType(get.ParameterType) != null); if (!canBeNull) { throw new Exception($"In action '{action.ActionName}', route parameter `{part}` is marked optional={get.IsOptional}, but the route " + $"template '{template}' is marked optional={templateOptional} and the type is non-nullable so is required."); } } var typeCode = Type.GetTypeCode(get.ParameterType); switch (typeCode) { case TypeCode.DateTime: case TypeCode.Object: throw new Exception($"In action '{action.ActionName}' parameter type '{get.ParameterType.Name}' is not suitable " + $"as a route parameter for template '{template}'. (Type code: {typeCode})"); } routeParameters.Add(get); routeJs.Add($"[{GetParamExecString(get)}, \"{get.ParameterName}\"]"); } foreach (var q in getParameters.Except(routeParameters)) { queryJs.Add($"[{GetParamExecString(q)}, \"{q.ParameterName}\", {q.IsOptional.ToString().ToLower()}]"); } return($"_build_url(this._basePath, [{String.Join(", ", routeJs)}], [{String.Join(", ", queryJs)}])"); }
protected virtual ApiParamDesc[] ValidateParameters(List <ApiParamDesc> parameters, ApiMethod httpMethod, string routeTemplate, out ApiParamDesc postParam, out ApiParamDesc modelParam) { var canHavePost = new[] { ApiMethod.Patch, ApiMethod.Post, ApiMethod.Put }.Contains(httpMethod); var postPossibilities = parameters .Where(p => !IsRouteParameter(p.ParameterName, routeTemplate)) .Where(p => p.Mode != ApiParameterMode.FromUri) .Where(p => p.Mode == ApiParameterMode.FromBody || TypeConverter.IsComplexType(p.ParameterType)) .ToArray(); if (postPossibilities.Length > 1) { throw new InvalidOperationException($"Invalid action, can't have more than one candidate for post parameters. Try using [FromBody] or [FromUri] to provide additional context. (at {routeTemplate}"); } var post = postPossibilities.FirstOrDefault(); if (!canHavePost && post != null) { // if there's only a single parameter in a get method, mvc will bind query parameters into it. if (post.Mode != ApiParameterMode.FromBody && parameters.Count == 1 && !IsRouteParameter(post.ParameterName, routeTemplate)) { var m = GetMembersAsParams(post.ParameterType).Select(kvp => new ApiParamDesc { ParameterName = kvp.Key, ParameterType = kvp.Value, IsOptional = true, Mode = ApiParameterMode.FromUri, }).ToList(); var inner = ValidateParameters(m, httpMethod, routeTemplate, out postParam, out modelParam); modelParam = post; postParam = null; return(inner); } throw new InvalidOperationException($"Invalid action, unable to map complex parameter '{post.ParameterName}' to {httpMethod} request as a message body is not allowed. (at {routeTemplate})"); } postParam = post; modelParam = null; return(parameters.Except(new[] { post }).ToArray()); }