diff --git a/src/main/resources/csharp/api.mustache b/src/main/resources/csharp/api.mustache index 0b0f36e1c38d..3d556fa72f85 100644 --- a/src/main/resources/csharp/api.mustache +++ b/src/main/resources/csharp/api.mustache @@ -45,6 +45,7 @@ // query params var queryParams = new Dictionary(); var headerParams = new Dictionary(); + var formParams = new Dictionary(); {{#requiredParamCount}} // verify required params are set @@ -53,9 +54,8 @@ } {{/requiredParamCount}} - string paramStr = null; {{#queryParams}}if ({{paramName}} != null){ - paramStr = ({{paramName}} != null && {{paramName}} is DateTime) ? ((DateTime)(object){{paramName}}).ToString("u") : Convert.ToString({{paramName}}); + string paramStr = ({{paramName}} is DateTime) ? ((DateTime)(object){{paramName}}).ToString("u") : Convert.ToString({{paramName}}); queryParams.Add("{{paramName}}", paramStr); } {{/queryParams}} @@ -63,17 +63,32 @@ {{#headerParams}}headerParams.Add("{{paramName}}", {{paramName}}); {{/headerParams}} - try { - var response = apiInvoker.invokeAPI(basePath, path, "{{httpMethod}}", queryParams, {{#bodyParam}}{{bodyParam}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}, headerParams); - if(response != null){ - return {{#returnType}}({{{returnType}}}) ApiInvoker.deserialize(response, typeof({{{returnType}}})){{/returnType}}; + {{#formParams}}if ({{paramName}} != null){ + if({{paramName}} is byte[]) { + formParams.Add("{{paramName}}", {{paramName}}); + } else { + string paramStr = ({{paramName}} is DateTime) ? ((DateTime)(object){{paramName}}).ToString("u") : Convert.ToString({{paramName}}); + formParams.Add("{{paramName}}", paramStr); } - else { - return {{#returnType}}null{{/returnType}}; + } + {{/formParams}} + + try { + if (typeof({{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}) == typeof(byte[])) { + var response = apiInvoker.invokeBinaryAPI(basePath, path, "GET", queryParams, null, headerParams, formParams); + return {{#returnType}}((object)response) as {{{returnType}}}{{/returnType}}; + } else { + var response = apiInvoker.invokeAPI(basePath, path, "{{httpMethod}}", queryParams, {{#bodyParam}}{{bodyParam}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}, headerParams, formParams); + if(response != null){ + return {{#returnType}}({{{returnType}}}) ApiInvoker.deserialize(response, typeof({{{returnType}}})){{/returnType}}; + } + else { + return {{#returnType}}null{{/returnType}}; + } } } catch (ApiException ex) { if(ex.ErrorCode == 404) { - return {{#returnType}} null{{/returnType}}; + return {{#returnType}}null{{/returnType}}; } else { throw ex; diff --git a/src/main/resources/csharp/apiInvoker.mustache b/src/main/resources/csharp/apiInvoker.mustache index 412f894792ce..f7c76ee2c569 100644 --- a/src/main/resources/csharp/apiInvoker.mustache +++ b/src/main/resources/csharp/apiInvoker.mustache @@ -44,7 +44,17 @@ } } - public string invokeAPI(string host, string path, string method, Dictionary queryParams, object body, Dictionary headerParams) { + public string invokeAPI(string host, string path, string method, Dictionary queryParams, object body, Dictionary headerParams, Dictionary formParams) + { + return invokeAPIInternal(host, path, method, false, queryParams, body, headerParams, formParams) as string; + } + + public byte[] invokeBinaryAPI(string host, string path, string method, Dictionary queryParams, object body, Dictionary headerParams, Dictionary formParams) + { + return invokeAPIInternal(host, path, method, true, queryParams, body, headerParams, formParams) as byte[]; + } + + private object invokeAPIInternal(string host, string path, string method, bool binaryResponse, Dictionary queryParams, object body, Dictionary headerParams, Dictionary formParams) { var b = new StringBuilder(); foreach (var queryParamItem in queryParams) @@ -60,9 +70,21 @@ host = host.EndsWith("/") ? host.Substring(0, host.Length - 1) : host; var client = WebRequest.Create(host + path + querystring); - client.ContentType = "application/json"; client.Method = method; + byte[] formData = null; + if (formParams.Count > 0) + { + string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid()); + client.ContentType = "multipart/form-data; boundary=" + formDataBoundary; + formData = GetMultipartFormData(formParams, formDataBoundary); + client.ContentLength = formData.Length; + } + else + { + client.ContentType = "application/json"; + } + foreach (var headerParamsItem in headerParams) { client.Headers.Add(headerParamsItem.Key, headerParamsItem.Value); @@ -79,9 +101,17 @@ case "POST": case "PUT": case "DELETE": - var swRequestWriter = new StreamWriter(client.GetRequestStream()); - swRequestWriter.Write(serialize(body)); - swRequestWriter.Close(); + using (Stream requestStream = client.GetRequestStream()) + { + if (formData != null) + { + requestStream.Write(formData, 0, formData.Length); + } + + var swRequestWriter = new StreamWriter(requestStream); + swRequestWriter.Write(serialize(body)); + swRequestWriter.Close(); + } break; default: throw new ApiException(500, "unknown method type " + method); @@ -96,10 +126,22 @@ throw new ApiException((int)webResponse.StatusCode, webResponse.StatusDescription); } - var responseReader = new StreamReader(webResponse.GetResponseStream()); - var responseData = responseReader.ReadToEnd(); - responseReader.Close(); - return responseData; + if (binaryResponse) + { + using (var memoryStream = new MemoryStream()) + { + webResponse.GetResponseStream().CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } + else + { + using (var responseReader = new StreamReader(webResponse.GetResponseStream())) + { + var responseData = responseReader.ReadToEnd(); + return responseData; + } + } } catch(WebException ex) { @@ -113,5 +155,53 @@ throw new ApiException(statusCode, ex.Message); } } + + private static byte[] GetMultipartFormData(Dictionary postParameters, string boundary) + { + Stream formDataStream = new System.IO.MemoryStream(); + bool needsCLRF = false; + + foreach (var param in postParameters) + { + // Thanks to feedback from commenters, add a CRLF to allow multiple parameters to be added. + // Skip it on the first parameter, add it to subsequent parameters. + if (needsCLRF) + formDataStream.Write(Encoding.UTF8.GetBytes("\r\n"), 0, Encoding.UTF8.GetByteCount("\r\n")); + + needsCLRF = true; + + if (param.Value is byte[]) + { + string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n", + boundary, + param.Key, + "application/octet-stream"); + formDataStream.Write(Encoding.UTF8.GetBytes(postData), 0, Encoding.UTF8.GetByteCount(postData)); + + // Write the file data directly to the Stream, rather than serializing it to a string. + formDataStream.Write((param.Value as byte[]), 0, (param.Value as byte[]).Length); + } + else + { + string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}", + boundary, + param.Key, + param.Value); + formDataStream.Write(Encoding.UTF8.GetBytes(postData), 0, Encoding.UTF8.GetByteCount(postData)); + } + } + + // Add the end of the request. Start with a newline + string footer = "\r\n--" + boundary + "--\r\n"; + formDataStream.Write(Encoding.UTF8.GetBytes(footer), 0, Encoding.UTF8.GetByteCount(footer)); + + // Dump the Stream into a byte[] + formDataStream.Position = 0; + byte[] formData = new byte[formDataStream.Length]; + formDataStream.Read(formData, 0, formData.Length); + formDataStream.Close(); + + return formData; + } } } diff --git a/src/main/scala/com/wordnik/swagger/codegen/BasicCSharpGenerator.scala b/src/main/scala/com/wordnik/swagger/codegen/BasicCSharpGenerator.scala index 873e66fd4499..3490ff00a7fa 100644 --- a/src/main/scala/com/wordnik/swagger/codegen/BasicCSharpGenerator.scala +++ b/src/main/scala/com/wordnik/swagger/codegen/BasicCSharpGenerator.scala @@ -41,7 +41,7 @@ class BasicCSharpGenerator extends BasicGenerator { * We are using csharp objects instead of primitives to avoid showing default * primitive values when the API returns missing data. For instance, having a * {"count":0} != count is unknown. You can change this to use primitives if you - * desire, but update the default values as well or they'll be set to null in + * desire, but update the default values as well or they'll be set to null in * variable declarations. */ override def typeMapping = Map( @@ -54,7 +54,9 @@ class BasicCSharpGenerator extends BasicGenerator { "double" -> "double?", "object" -> "object", "Date" -> "DateTime?", - "date" -> "DateTime?") + "date" -> "DateTime?", + "File" -> "byte[]", + "file" -> "byte[]") // location of templates override def templateDir = "csharp" @@ -70,11 +72,11 @@ class BasicCSharpGenerator extends BasicGenerator { // template used for models apiTemplateFiles += "api.mustache" -> ".cs" - override def reservedWords = Set("abstract", "continue", "for", "new", "switch", "assert", - "default", "if", "package", "synchronized", "do", "goto", "private", "this", "break", - "implements", "protected", "throw", "else", "import", "public", "throws", "case", - "enum", "instanceof", "return", "transient", "catch", "extends", "try", "final", - "interface", "static", "void", "class", "finally", "strictfp", "volatile", "const", + override def reservedWords = Set("abstract", "continue", "for", "new", "switch", "assert", + "default", "if", "package", "synchronized", "do", "goto", "private", "this", "break", + "implements", "protected", "throw", "else", "import", "public", "throws", "case", + "enum", "instanceof", "return", "transient", "catch", "extends", "try", "final", + "interface", "static", "void", "class", "finally", "strictfp", "volatile", "const", "native", "super", "while") // import/require statements for specific datatypes @@ -180,7 +182,7 @@ class BasicCSharpGenerator extends BasicGenerator { } override def escapeReservedWord(word: String) = { - if (reservedWords.contains(word)) + if (reservedWords.contains(word)) throw new Exception("reserved word " + "\"" + word + "\" not allowed") else word } @@ -193,8 +195,8 @@ class BasicCSharpGenerator extends BasicGenerator { capitalize(name) } - def capitalize(s: String) = { - s(0).toUpper + s.substring(1, s.length).toLowerCase + def capitalize(s: String) = { + s(0).toUpper + s.substring(1, s.length).toLowerCase }*/ // supporting classes diff --git a/src/main/scala/com/wordnik/swagger/codegen/Codegen.scala b/src/main/scala/com/wordnik/swagger/codegen/Codegen.scala index 0bc2d9c14bcf..81435d8f4342 100644 --- a/src/main/scala/com/wordnik/swagger/codegen/Codegen.scala +++ b/src/main/scala/com/wordnik/swagger/codegen/Codegen.scala @@ -309,7 +309,7 @@ class Codegen(config: CodegenConfig) { val ComplexTypeMatcher(basePart) = operation.responseClass properties += "returnType" -> config.processResponseDeclaration(operation.responseClass.replaceAll(basePart, config.processResponseClass(basePart).get)) - properties += "returnContainer" -> (operation.responseClass.substring(0, n)) + properties += "returnContainer" -> config.processResponseClass(operation.responseClass.substring(0, n)) properties += "returnBaseType" -> config.processResponseClass(basePart) properties += "returnTypeIsPrimitive" -> { (config.languageSpecificPrimitives.contains(basePart) || primitives.contains(basePart)) match { diff --git a/src/test/scala/BasicScalaGeneratorTest.scala b/src/test/scala/BasicScalaGeneratorTest.scala index d2147f78cb48..9bcea0af9d07 100644 --- a/src/test/scala/BasicScalaGeneratorTest.scala +++ b/src/test/scala/BasicScalaGeneratorTest.scala @@ -211,7 +211,7 @@ class BasicScalaGeneratorTest extends FlatSpec with ShouldMatchers { m("returnType") should be (Some("List[Pet]")) m("returnTypeIsPrimitive") should be (None) m("pathParams").asInstanceOf[List[_]].size should be (0) - m("returnContainer") should be ("List") + m("returnContainer") should be (Some("List")) m("requiredParamCount") should be ("1") val queryParams = m("queryParams").asInstanceOf[List[_]] @@ -248,7 +248,7 @@ class BasicScalaGeneratorTest extends FlatSpec with ShouldMatchers { m("returnType") should be (Some("List[Pet]")) m("returnTypeIsPrimitive") should be (None) m("pathParams").asInstanceOf[List[_]].size should be (0) - m("returnContainer") should be ("List") + m("returnContainer") should be (Some("List")) m("requiredParamCount") should be ("1") val queryParams = m("queryParams").asInstanceOf[List[_]] diff --git a/src/test/scala/CodegenTest.scala b/src/test/scala/CodegenTest.scala new file mode 100644 index 000000000000..26156bb8eb50 --- /dev/null +++ b/src/test/scala/CodegenTest.scala @@ -0,0 +1,55 @@ +/** + * Copyright 2014 Wordnik, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.wordnik.swagger.codegen.Codegen +import com.wordnik.swagger.codegen.BasicJavaGenerator +import com.wordnik.swagger.codegen.model._ + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers + +import scala.reflect.BeanProperty + +@RunWith(classOf[JUnitRunner]) +class CodegenTest extends FlatSpec with ShouldMatchers { + + val subject = new Codegen(new BasicJavaGenerator) + + val testOp = new Operation("GET", + "List All Contacts", + "", + "Array[ContactData]", + "listContacts", + 0, + List.empty, + List.empty, + List.empty, + List.empty, + List.empty, + List.empty, + None) + + behavior of "Codegen" + /* + * A return specified as "Array" should map to "List" + */ + it should "recognize the returnContainer as a List" in { + val map = subject.apiToMap("/contacts", testOp) + map("returnContainer") should be (Some("List")) + } +}