[haskell-http-client] add support for auth methods (#6622)

* add support for auth methods

* use newtypes for required params

* fix duplicate operationId issues

* prevent aliasing of vendorextension references in fromOperation

* add --fast to stack ci build
This commit is contained in:
Jon Schoning
2017-10-07 04:12:48 -05:00
committed by wing328
parent 0db4b32384
commit 5b32e886f4
47 changed files with 4923 additions and 4097 deletions

View File

@@ -5,15 +5,14 @@ import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.MapProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.*;
import java.util.*;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import io.swagger.models.auth.SecuritySchemeDefinition;
import io.swagger.codegen.CliOption;
import io.swagger.codegen.CodegenConstants;
import io.swagger.codegen.CodegenModel;
@@ -26,6 +25,7 @@ import java.io.IOException;
import java.io.File;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.text.WordUtils;
import java.util.regex.Matcher;
@@ -65,8 +65,8 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
static final String MEDIA_IS_JSON = "x-mediaIsJson";
protected Map<String, CodegenParameter> uniqueOptionalParamsByName = new HashMap<String, CodegenParameter>();
protected Map<String, CodegenModel> modelNames = new HashMap<String, CodegenModel>();
protected Map<String, CodegenParameter> uniqueParamsByName = new HashMap<String, CodegenParameter>();
protected Set<String> typeNames = new HashSet<String>();
protected Map<String, Map<String,String>> allMimeTypes = new HashMap<String, Map<String,String>>();
protected Map<String, String> knownMimeDataTypes = new HashMap<String, String>();
protected Map<String, Set<String>> modelMimeTypes = new HashMap<String, Set<String>>();
@@ -466,42 +466,49 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map<String, Model> definitions, Swagger swagger) {
CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, swagger);
op.vendorExtensions.put("x-baseOperationId", op.operationId);
// prevent aliasing/sharing of operation.vendorExtensions reference
op.vendorExtensions = new LinkedHashMap();
String operationType = toTypeName("Op", op.operationId);
op.vendorExtensions.put("x-operationType", operationType);
typeNames.add(operationType);
op.vendorExtensions.put("x-haddockPath", String.format("%s %s", op.httpMethod, op.path.replace("/", "\\/")));
op.operationId = toVarName(op.operationId);
op.vendorExtensions.put("x-operationType", toTypeName("Op", op.operationId));
op.vendorExtensions.put("x-hasBodyOrFormParam", op.getHasBodyParam() || op.getHasFormParams());
for (CodegenParameter param : op.allParams) {
param.vendorExtensions.put("x-operationType", WordUtils.capitalize(op.operationId));
param.vendorExtensions = new LinkedHashMap(); // prevent aliasing/sharing
param.vendorExtensions.put("x-operationType", operationType);
param.vendorExtensions.put("x-isBodyOrFormParam", param.isBodyParam || param.isFormParam);
if (!StringUtils.isBlank(param.collectionFormat)) {
param.vendorExtensions.put("x-collectionFormat", mapCollectionFormat(param.collectionFormat));
}
if (!param.required) {
if(!param.required) {
op.vendorExtensions.put("x-hasOptionalParams", true);
}
if (typeMapping.containsKey(param.dataType) || param.isPrimitiveType || param.isListContainer || param.isMapContainer || param.isFile) {
String paramNameType = toTypeName("Param", param.paramName);
if (uniqueOptionalParamsByName.containsKey(paramNameType)) {
CodegenParameter lastParam = this.uniqueOptionalParamsByName.get(paramNameType);
if (uniqueParamsByName.containsKey(paramNameType)) {
CodegenParameter lastParam = this.uniqueParamsByName.get(paramNameType);
if (lastParam.dataType != null && lastParam.dataType.equals(param.dataType)) {
param.vendorExtensions.put("x-duplicate", true);
} else {
paramNameType = paramNameType + param.dataType;
while (modelNames.containsKey(paramNameType)) {
while (typeNames.contains(paramNameType)) {
paramNameType = generateNextName(paramNameType);
}
uniqueParamsByName.put(paramNameType, param);
}
} else {
while (modelNames.containsKey(paramNameType)) {
while (typeNames.contains(paramNameType)) {
paramNameType = generateNextName(paramNameType);
}
uniqueOptionalParamsByName.put(paramNameType, param);
uniqueParamsByName.put(paramNameType, param);
}
param.vendorExtensions.put("x-paramNameType", paramNameType);
op.vendorExtensions.put("x-hasBodyOrFormParam", op.getHasBodyParam() || op.getHasFormParams());
typeNames.add(paramNameType);
}
}
if (op.getHasPathParams()) {
@@ -572,7 +579,18 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
return op;
}
public List<CodegenSecurity> fromSecurity(Map<String, SecuritySchemeDefinition> schemes) {
List<CodegenSecurity> secs = super.fromSecurity(schemes);
for(CodegenSecurity sec : secs) {
String prefix = "";
if(sec.isBasic) prefix = "AuthBasic";
if(sec.isApiKey) prefix = "AuthApiKey";
if(sec.isOAuth) prefix = "AuthOAuth";
sec.name = prefix + toTypeName("",sec.name);
}
return secs;
}
@Override
public Map<String, Object> postProcessOperations(Map<String, Object> objs) {
@@ -586,6 +604,7 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
additionalProperties.put("x-hasUnknownMimeTypes", !unknownMimeTypes.isEmpty());
additionalProperties.put("x-unknownMimeTypes", unknownMimeTypes);
additionalProperties.put("x-allUniqueParams", uniqueParamsByName.values());
return ret;
}
@@ -619,12 +638,13 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
public CodegenModel fromModel(String name, Model mod, Map<String, Model> allDefinitions) {
CodegenModel model = super.fromModel(name, mod, allDefinitions);
while (uniqueOptionalParamsByName.containsKey(model.classname)) {
while (typeNames.contains(model.classname)) {
model.classname = generateNextName(model.classname);
}
typeNames.add(model.classname);
// From the model name, compute the prefix for the fields.
String prefix = WordUtils.uncapitalize(model.classname);
String prefix = StringUtils.uncapitalize(model.classname);
for (CodegenProperty prop : model.vars) {
prop.name = toVarName(prefix, prop.name);
}
@@ -635,7 +655,6 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
return model;
}
modelNames.put(model.classname, model);
return model;
}
@@ -674,6 +693,7 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
if(StringUtils.isBlank(mediaType)) return;
String mimeType = getMimeDataType(mediaType);
typeNames.add(mimeType);
m.put(MEDIA_DATA_TYPE, mimeType);
if (isJsonMimeType(mediaType)) {
m.put(MEDIA_IS_JSON, "true");
@@ -761,6 +781,7 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
}
return false;
}
@Override
public String toVarName(String name) {
return toVarName("", name);
@@ -794,8 +815,28 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
return toTypeName("Model", name);
}
public String toTypeName(String prefix, String name) {
name = camelize(underscore(sanitizeName(name)));
name = escapeIdentifier(prefix, camelize(sanitizeName(name)));
return name;
}
@Override
public String toOperationId(String operationId) {
if (StringUtils.isEmpty(operationId)) {
throw new RuntimeException("Empty method/operation name (operationId) not allowed");
}
operationId = escapeIdentifier("op",camelize(sanitizeName(operationId), true));
String uniqueName = operationId;
String uniqueNameType = toTypeName("Op", operationId);
while (typeNames.contains(uniqueNameType)) {
uniqueName = generateNextName(uniqueName);
uniqueNameType = toTypeName("Op", uniqueName);
}
typeNames.add(uniqueNameType);
if(!operationId.equals(uniqueName)) {
LOGGER.warn("generated unique operationId `" + uniqueName + "`");
}
return uniqueName;
}
public String escapeIdentifier(String prefix, String name) {
if(StringUtils.isBlank(prefix)) return name;
if (isReservedWord(name)) {
@@ -815,4 +856,65 @@ public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenC
static boolean isJsonMimeType(String mime) {
return mime != null && JSON_MIME_PATTERN.matcher(mime).matches();
}
@Override
public String toDefaultValue(Property p) {
if (p instanceof StringProperty) {
StringProperty dp = (StringProperty) p;
if (dp.getDefault() != null) {
return "\"" + escapeText(dp.getDefault()) + "\"";
}
} else if (p instanceof BooleanProperty) {
BooleanProperty dp = (BooleanProperty) p;
if (dp.getDefault() != null) {
if (dp.getDefault().toString().equalsIgnoreCase("false"))
return "False";
else
return "True";
}
} else if (p instanceof DoubleProperty) {
DoubleProperty dp = (DoubleProperty) p;
if (dp.getDefault() != null) {
return dp.getDefault().toString();
}
} else if (p instanceof FloatProperty) {
FloatProperty dp = (FloatProperty) p;
if (dp.getDefault() != null) {
return dp.getDefault().toString();
}
} else if (p instanceof IntegerProperty) {
IntegerProperty dp = (IntegerProperty) p;
if (dp.getDefault() != null) {
return dp.getDefault().toString();
}
} else if (p instanceof LongProperty) {
LongProperty dp = (LongProperty) p;
if (dp.getDefault() != null) {
return dp.getDefault().toString();
}
}
return null;
}
// override with any special text escaping logic
@SuppressWarnings("static-method")
public String escapeText(String input) {
if (input == null) {
return input;
}
// remove \t, \n, \r
// replace \ with \\
// replace " with \"
// outter unescape to retain the original multi-byte characters
// finally escalate characters avoiding code injection
return escapeUnsafeCharacters(
StringEscapeUtils.unescapeJava(
StringEscapeUtils.escapeJava(input)
.replace("\\/", "/"))
.replaceAll("[\\t\\n\\r]"," ")
.replace("\\", "\\\\")
.replace("\"", "\\\""));
}
}

View File

@@ -3,16 +3,17 @@
Module : {{title}}.API
-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-unused-binds -fno-warn-unused-imports #-}
module {{title}}.API where
@@ -31,6 +32,7 @@ import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy.Char8 as BCL
import qualified Data.ByteString.Base64 as B64
import qualified Network.HTTP.Client.MultipartFormData as NH
import qualified Network.HTTP.Media as ME
@@ -40,7 +42,7 @@ import qualified Web.HttpApiData as WH
import qualified Web.FormUrlEncoded as WH
import qualified Data.CaseInsensitive as CI
import qualified Data.Data as P (Typeable)
import qualified Data.Data as P (Typeable, TypeRep, typeOf, typeRep)
import qualified Data.Foldable as P
import qualified Data.Map as Map
import qualified Data.Set as Set
@@ -76,30 +78,31 @@ import qualified Prelude as P
-- {{/summary}}{{#notes}}
-- {{{.}}}
-- {{/notes}}{{#hasAuthMethods}}
-- AuthMethod: {{#authMethods}}{{{name}}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}
-- AuthMethod: {{#authMethods}}'{{{name}}}'{{#hasMore}}, {{/hasMore}}{{/authMethods}}
-- {{/hasAuthMethods}}{{#vendorExtensions.x-hasUnknownReturn}}
-- Note: Has 'Produces' instances, but no response schema
-- {{/vendorExtensions.x-hasUnknownReturn}}
{{operationId}}
:: {{#vendorExtensions.x-hasBodyOrFormParam}}(Consumes {{{vendorExtensions.x-operationType}}} contentType{{#allParams}}{{#isBodyParam}}{{#required}}, MimeRender contentType {{dataType}}{{/required}}{{/isBodyParam}}{{/allParams}})
:: {{#vendorExtensions.x-hasBodyOrFormParam}}(Consumes {{{vendorExtensions.x-operationType}}} contentType{{#allParams}}{{#isBodyParam}}{{#required}}, MimeRender contentType {{#vendorExtensions.x-paramNameType}}{{{.}}}{{/vendorExtensions.x-paramNameType}}{{^vendorExtensions.x-paramNameType}}{{{dataType}}}{{/vendorExtensions.x-paramNameType}}{{/required}}{{/isBodyParam}}{{/allParams}})
=> contentType -- ^ request content-type ('MimeType')
-> {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{dataType}} -- ^ "{{{paramName}}}"{{#description}} - {{/description}} {{{description}}}
-> {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{#vendorExtensions.x-paramNameType}}{{{.}}}{{/vendorExtensions.x-paramNameType}}{{^vendorExtensions.x-paramNameType}}{{{dataType}}}{{/vendorExtensions.x-paramNameType}} -- ^ "{{{paramName}}}"{{#description}} - {{/description}} {{{description}}}
-> {{/required}}{{/allParams}}{{requestType}} {{{vendorExtensions.x-operationType}}} {{#vendorExtensions.x-hasBodyOrFormParam}}contentType{{/vendorExtensions.x-hasBodyOrFormParam}}{{^vendorExtensions.x-hasBodyOrFormParam}}MimeNoContent{{/vendorExtensions.x-hasBodyOrFormParam}} {{vendorExtensions.x-returnType}}
{{operationId}} {{#vendorExtensions.x-hasBodyOrFormParam}}_ {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{{paramName}}} {{/required}}{{/allParams}}=
{{operationId}} {{#vendorExtensions.x-hasBodyOrFormParam}}_ {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{#isBodyParam}}{{{paramName}}}{{/isBodyParam}}{{^isBodyParam}}({{{vendorExtensions.x-paramNameType}}} {{{paramName}}}){{/isBodyParam}} {{/required}}{{/allParams}}=
_mkRequest "{{httpMethod}}" [{{#pathParams}}{{#vendorExtensions.x-pathPrefix}}"{{.}}",{{/vendorExtensions.x-pathPrefix}}toPath {{{paramName}}}{{#hasMore}},{{/hasMore}}{{/pathParams}}{{#vendorExtensions.x-pathSuffix}}{{#vendorExtensions.x-hasPathParams}},{{/vendorExtensions.x-hasPathParams}}"{{.}}"{{/vendorExtensions.x-pathSuffix}}]{{#allParams}}{{#required}}
{{#isHeaderParam}}`setHeader` {{>_headerColl}} ("{{{baseName}}}", {{{paramName}}}){{/isHeaderParam}}{{#isQueryParam}}`_setQuery` {{>_queryColl}} ("{{{baseName}}}", Just {{{paramName}}}){{/isQueryParam}}{{#isFormParam}}{{#isFile}}`_addMultiFormPart` NH.partFileSource "{{{baseName}}}" {{{paramName}}}{{/isFile}}{{^isFile}}{{#isMultipart}}`_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData {{{paramName}}}){{/isMultipart}}{{^isMultipart}}`_addForm` {{>_formColl}} ("{{{baseName}}}", {{{paramName}}}){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{#isBodyParam}}`setBodyParam` {{{paramName}}}{{/isBodyParam}}{{/required}}{{/allParams}}{{#isDeprecated}}
{{#isHeaderParam}}`setHeader` {{>_headerColl}} ("{{{baseName}}}", {{{paramName}}}){{/isHeaderParam}}{{#isQueryParam}}`setQuery` {{>_queryColl}} ("{{{baseName}}}", Just {{{paramName}}}){{/isQueryParam}}{{#isFormParam}}{{#isFile}}`_addMultiFormPart` NH.partFileSource "{{{baseName}}}" {{{paramName}}}{{/isFile}}{{^isFile}}{{#isMultipart}}`_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData {{{paramName}}}){{/isMultipart}}{{^isMultipart}}`addForm` {{>_formColl}} ("{{{baseName}}}", {{{paramName}}}){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{#isBodyParam}}`setBodyParam` {{{paramName}}}{{/isBodyParam}}{{/required}}{{/allParams}}{{#authMethods}}
`_hasAuthType` (P.Proxy :: P.Proxy {{name}}){{/authMethods}}{{#isDeprecated}}
{-# DEPRECATED {{operationId}} "" #-}{{/isDeprecated}}
data {{{vendorExtensions.x-operationType}}} {{#allParams}}{{#isBodyParam}}{{#description}}
-- | /Body Param/ "{{{baseName}}}" - {{{description}}}{{/description}}
instance HasBodyParam {{{vendorExtensions.x-operationType}}} {{{dataType}}}{{/isBodyParam}}{{/allParams}} {{#vendorExtensions.x-hasOptionalParams}}{{#allParams}}{{^isBodyParam}}{{^required}}{{#description}}
instance HasBodyParam {{{vendorExtensions.x-operationType}}} {{#vendorExtensions.x-paramNameType}}{{{.}}}{{/vendorExtensions.x-paramNameType}}{{^vendorExtensions.x-paramNameType}}{{{dataType}}}{{/vendorExtensions.x-paramNameType}}{{/isBodyParam}}{{/allParams}} {{#vendorExtensions.x-hasOptionalParams}}{{#allParams}}{{^isBodyParam}}{{^required}}{{#description}}
-- | /Optional Param/ "{{{baseName}}}" - {{{description}}}{{/description}}
instance HasOptionalParam {{{vendorExtensions.x-operationType}}} {{{vendorExtensions.x-paramNameType}}} where
applyOptionalParam req ({{{vendorExtensions.x-paramNameType}}} xs) =
{{#isHeaderParam}}req `setHeader` {{>_headerColl}} ("{{{baseName}}}", xs){{/isHeaderParam}}{{#isQueryParam}}req `_setQuery` {{>_queryColl}} ("{{{baseName}}}", Just xs){{/isQueryParam}}{{#isFormParam}}{{#isFile}}req `_addMultiFormPart` NH.partFileSource "{{{baseName}}}" xs{{/isFile}}{{^isFile}}{{#isMultipart}}req `_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData xs){{/isMultipart}}{{^isMultipart}}req `_addForm` {{>_formColl}} ("{{{baseName}}}", xs){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{/required}}{{/isBodyParam}}{{/allParams}}{{/vendorExtensions.x-hasOptionalParams}}{{#hasConsumes}}
{{#isHeaderParam}}req `setHeader` {{>_headerColl}} ("{{{baseName}}}", xs){{/isHeaderParam}}{{#isQueryParam}}req `setQuery` {{>_queryColl}} ("{{{baseName}}}", Just xs){{/isQueryParam}}{{#isFormParam}}{{#isFile}}req `_addMultiFormPart` NH.partFileSource "{{{baseName}}}" xs{{/isFile}}{{^isFile}}{{#isMultipart}}req `_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData xs){{/isMultipart}}{{^isMultipart}}req `addForm` {{>_formColl}} ("{{{baseName}}}", xs){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{/required}}{{/isBodyParam}}{{/allParams}}{{/vendorExtensions.x-hasOptionalParams}}{{#hasConsumes}}
{{#consumes}}-- | @{{{mediaType}}}@
instance Consumes {{{vendorExtensions.x-operationType}}} {{{x-mediaDataType}}}
@@ -135,11 +138,12 @@ class HasOptionalParam req param where
infixl 2 -&-
-- * Optional Request Parameter Types
-- * Request Parameter Types
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}{{#vendorExtensions.x-hasOptionalParams}}{{#allParams}}{{^required}}{{^vendorExtensions.x-duplicate}}
newtype {{{vendorExtensions.x-paramNameType}}} = {{{vendorExtensions.x-paramNameType}}} { un{{{vendorExtensions.x-paramNameType}}} :: {{{dataType}}} } deriving (P.Eq, P.Show)
{{/vendorExtensions.x-duplicate}}{{/required}}{{/allParams}}{{/vendorExtensions.x-hasOptionalParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
{{#x-allUniqueParams}}
-- | {{{vendorExtensions.x-paramNameType}}}
newtype {{{vendorExtensions.x-paramNameType}}} = {{{vendorExtensions.x-paramNameType}}} { un{{{vendorExtensions.x-paramNameType}}} :: {{{dataType}}} } deriving (P.Eq, P.Show{{#isBodyParam}}, A.ToJSON{{/isBodyParam}})
{{/x-allUniqueParams}}
-- * {{requestType}}
@@ -148,6 +152,7 @@ data {{requestType}} req contentType res = {{requestType}}
{ rMethod :: NH.Method -- ^ Method of {{requestType}}
, rUrlPath :: [BCL.ByteString] -- ^ Endpoint of {{requestType}}
, rParams :: Params -- ^ params of {{requestType}}
, rAuthTypes :: [P.TypeRep] -- ^ types of auth methods
}
deriving (P.Show)
@@ -166,6 +171,11 @@ rParamsL :: Lens_' ({{requestType}} req contentType res) Params
rParamsL f {{requestType}}{..} = (\rParams -> {{requestType}} { rParams, ..} ) <$> f rParams
{-# INLINE rParamsL #-}
-- | 'rParams' Lens
rAuthTypesL :: Lens_' ({{requestType}} req contentType res) [P.TypeRep]
rAuthTypesL f {{requestType}}{..} = (\rAuthTypes -> {{requestType}} { rAuthTypes, ..} ) <$> f rAuthTypes
{-# INLINE rAuthTypesL #-}
-- | Request Params
data Params = Params
{ paramsQuery :: NH.Query
@@ -203,7 +213,7 @@ data ParamBody
_mkRequest :: NH.Method -- ^ Method
-> [BCL.ByteString] -- ^ Endpoint
-> {{requestType}} req contentType res -- ^ req: Request Type, res: Response Type
_mkRequest m u = {{requestType}} m u _mkParams
_mkRequest m u = {{requestType}} m u _mkParams []
_mkParams :: Params
_mkParams = Params [] [] ParamBodyNone
@@ -235,8 +245,8 @@ _setAcceptHeader req accept =
Just m -> req `setHeader` [("accept", BC.pack $ P.show m)]
Nothing -> req `removeHeader` ["accept"]
_setQuery :: {{requestType}} req contentType res -> [NH.QueryItem] -> {{requestType}} req contentType res
_setQuery req query =
setQuery :: {{requestType}} req contentType res -> [NH.QueryItem] -> {{requestType}} req contentType res
setQuery req query =
req &
L.over
(rParamsL . paramsQueryL)
@@ -244,8 +254,8 @@ _setQuery req query =
where
cifst = CI.mk . P.fst
_addForm :: {{requestType}} req contentType res -> WH.Form -> {{requestType}} req contentType res
_addForm req newform =
addForm :: {{requestType}} req contentType res -> WH.Form -> {{requestType}} req contentType res
addForm req newform =
let form = case paramsBody (rParams req) of
ParamBodyFormUrlEncoded _form -> _form
_ -> mempty
@@ -266,6 +276,9 @@ _setBodyLBS :: {{requestType}} req contentType res -> BL.ByteString -> {{request
_setBodyLBS req body =
req & L.set (rParamsL . paramsBodyL) (ParamBodyBL body)
_hasAuthType :: AuthMethod authMethod => {{requestType}} req contentType res -> P.Proxy authMethod -> {{requestType}} req contentType res
_hasAuthType req proxy =
req & L.over rAuthTypesL (P.typeRep proxy :)
-- ** Params Utils
@@ -330,3 +343,49 @@ _toCollA' c encode one xs = case c of
{-# INLINE expandList #-}
{-# INLINE combine #-}
-- * AuthMethods
-- | Provides a method to apply auth methods to requests
class P.Typeable a => AuthMethod a where
applyAuthMethod :: {{requestType}} req contentType res -> a -> {{requestType}} req contentType res
-- | An existential wrapper for any AuthMethod
data AnyAuthMethod = forall a. AuthMethod a => AnyAuthMethod a deriving (P.Typeable)
instance AuthMethod AnyAuthMethod where applyAuthMethod req (AnyAuthMethod a) = applyAuthMethod req a
{{#authMethods}}{{#isBasic}}-- ** {{name}}
data {{name}} =
{{name}} B.ByteString B.ByteString -- ^ username password
deriving (P.Eq, P.Show, P.Typeable)
instance AuthMethod {{name}} where
applyAuthMethod req a@({{name}} user pw) =
if (P.typeOf a `P.elem` rAuthTypes req)
then req `setHeader` toHeader ("Authorization", T.decodeUtf8 cred)
else req
where cred = BC.append "Basic " (B64.encode $ BC.concat [ user, ":", pw ])
{{/isBasic}}{{#isApiKey}}-- ** {{name}}
data {{name}} =
{{name}} Text -- ^ secret
deriving (P.Eq, P.Show, P.Typeable)
instance AuthMethod {{name}} where
applyAuthMethod req a@({{name}} secret) =
if (P.typeOf a `P.elem` rAuthTypes req)
then req {{#isKeyInHeader}}`setHeader` toHeader ("{{keyParamName}}", secret){{/isKeyInHeader}}{{^isKeyInHeader}}`setQuery` toQuery ("{{keyParamName}}", Just secret){{/isKeyInHeader}}
else req
{{/isApiKey}}{{#isOAuth}}-- ** {{name}}
data {{name}} =
{{name}} Text -- ^ secret
deriving (P.Eq, P.Show, P.Typeable)
instance AuthMethod {{name}} where
applyAuthMethod req a@({{name}} secret) =
if (P.typeOf a `P.elem` rAuthTypes req)
then req `setHeader` toHeader ("Authorization", "Bearer " <> secret)
else req
{{/isOAuth}}{{/authMethods}}

View File

@@ -59,6 +59,7 @@ data {{configType}} = {{configType}}
, configUserAgent :: Text -- ^ user-agent supplied in the Request
, configLogExecWithContext :: LogExecWithContext -- ^ Run a block using a Logger instance
, configLogContext :: LogContext -- ^ Configures the logger
, configAuthMethods :: [AnyAuthMethod] -- ^ List of configured auth methods
}
-- | display the config
@@ -87,13 +88,21 @@ newConfig = do
, configUserAgent = "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}{{{artifactId}}}/{{{artifactVersion}}}{{/httpUserAgent}}"
, configLogExecWithContext = runDefaultLogExecWithContext
, configLogContext = logCxt
, configAuthMethods = []
}
-- | updates config use AuthMethod on matching requests
addAuthMethod :: AuthMethod auth => {{configType}} -> auth -> {{configType}}
addAuthMethod config@{{configType}} {configAuthMethods = as} a =
config { configAuthMethods = AnyAuthMethod a : as}
-- | updates the config to use stdout logging
withStdoutLogging :: {{configType}} -> IO {{configType}}
withStdoutLogging p = do
logCxt <- stdoutLoggingContext (configLogContext p)
return $ p { configLogExecWithContext = stdoutLoggingExec, configLogContext = logCxt }
-- | updates the config to use stderr logging
withStderrLogging :: {{configType}} -> IO {{configType}}
withStderrLogging p = do
logCxt <- stderrLoggingContext (configLogContext p)
@@ -226,7 +235,9 @@ _toInitRequest
-> IO (InitRequest req contentType res accept) -- ^ initialized request
_toInitRequest config req0 accept = do
parsedReq <- NH.parseRequest $ BCL.unpack $ BCL.append (configHost config) (BCL.concat (rUrlPath req0))
let req1 = _setAcceptHeader req0 accept & _setContentTypeHeader
let req1 = _applyAuthMethods req0 config
& _setContentTypeHeader
& flip _setAcceptHeader accept
reqHeaders = ("User-Agent", WH.toHeader (configUserAgent config)) : paramsHeaders (rParams req1)
reqQuery = NH.renderQuery True (paramsQuery (rParams req1))
pReq = parsedReq { NH.method = (rMethod req1)
@@ -242,6 +253,16 @@ _toInitRequest config req0 accept = do
pure (InitRequest outReq)
-- | apply all matching AuthMethods in config to request
_applyAuthMethods
:: {{requestType}} req contentType res
-> {{configType}}
-> {{requestType}} req contentType res
_applyAuthMethods req {{configType}} {configAuthMethods = as} =
foldl go req as
where
go r (AnyAuthMethod a) = r `applyAuthMethod` a
-- | modify the underlying Request
modifyInitRequest :: InitRequest req contentType res accept -> (NH.Request -> NH.Request) -> InitRequest req contentType res accept
modifyInitRequest (InitRequest req) f = InitRequest (f req)

View File

@@ -56,7 +56,7 @@ import qualified Prelude as P
{{#model}}
-- ** {{classname}}
-- |{{#title}}
-- | {{classname}}{{#title}}
-- {{{.}}}
-- {{/title}}{{#description}}
-- {{{.}}}{{/description}}
@@ -65,12 +65,15 @@ data {{classname}} = {{classname}}
, {{/hasMore}}{{/vars}}
} deriving (P.Show,P.Eq,P.Typeable{{#modelDeriving}},{{modelDeriving}}{{/modelDeriving}})
-- | FromJSON {{classname}}
instance A.FromJSON {{classname}} where
parseJSON = A.withObject "{{classname}}" $ \o ->
{{^hasVars}}pure {{/hasVars}}{{classname}}
{{#hasVars}}<$>{{/hasVars}}{{#vars}} (o {{#required}}.: {{/required}}{{^required}}{{^allowFromJsonNulls}}.:!{{/allowFromJsonNulls}}{{#allowFromJsonNulls}}.:?{{/allowFromJsonNulls}}{{/required}} "{{baseName}}"){{#hasMore}}
<*>{{/hasMore}}{{/vars}}
-- | ToJSON {{classname}}
instance A.ToJSON {{classname}} where
toJSON {{classname}} {{#hasVars}}{..}{{/hasVars}} =
{{^allowToJsonNulls}}_omitNulls{{/allowToJsonNulls}}{{#allowToJsonNulls}}A.object{{/allowToJsonNulls}}
@@ -79,12 +82,14 @@ instance A.ToJSON {{classname}} where
]
{{#vendorExtensions.x-hasMimeFormUrlEncoded}}
-- | FromForm {{classname}}
instance WH.FromForm {{classname}} where
fromForm f =
{{^hasVars}}pure {{/hasVars}}{{classname}}
{{#hasVars}}<$>{{/hasVars}}{{#vars}} ({{#required}}WH.parseUnique {{/required}}{{^required}}WH.parseMaybe {{/required}}"{{baseName}}" f){{#hasMore}}
<*>{{/hasMore}}{{/vars}}
-- | ToForm {{classname}}
instance WH.ToForm {{classname}} where
toForm {{classname}} {{#hasVars}}{..}{{/hasVars}} =
WH.Form $ HM.fromList $ P.catMaybes $
@@ -111,21 +116,23 @@ mk{{classname}} {{#requiredVars}}{{name}} {{/requiredVars}}=
-- * Utils
-- | Removes Null fields. (OpenAPI-Specification 2.0 does not allow Null in JSON)
_omitNulls :: [(Text, A.Value)] -> A.Value
_omitNulls = A.object . P.filter notNull
where
notNull (_, A.Null) = False
notNull _ = True
-- | Encodes fields using WH.toQueryParam
_toFormItem :: (WH.ToHttpApiData a, Functor f) => t -> f a -> f (t, [Text])
_toFormItem name x = (name,) . (:[]) . WH.toQueryParam <$> x
-- | Collapse (Just "") to Nothing
_emptyToNothing :: Maybe String -> Maybe String
_emptyToNothing (Just "") = Nothing
_emptyToNothing x = x
{-# INLINE _emptyToNothing #-}
-- | Collapse (Just mempty) to Nothing
_memptyToNothing :: (P.Monoid a, P.Eq a) => Maybe a -> Maybe a
_memptyToNothing (Just x) | x P.== P.mempty = Nothing
_memptyToNothing x = x
@@ -158,6 +165,7 @@ _showDateTime =
{{^dateTimeFormat}}TI.formatISO8601Millis{{/dateTimeFormat}}{{#dateTimeFormat}}TI.formatTime TI.defaultTimeLocale "{{{dateTimeFormat}}}"{{/dateTimeFormat}}
{-# INLINE _showDateTime #-}
-- | parse an ISO8601 date-time string
_parseISO8601 :: (TI.ParseTime t, Monad m, Alternative m) => String -> m t
_parseISO8601 t =
P.asum $

View File

@@ -46,10 +46,6 @@ haskell-http-client
### Unsupported Swagger Features
* Auth Methods (https://swagger.io/docs/specification/2-0/authentication/)
- use `setHeader` to add any required headers to requests
* Model Inheritance
* Default Parameter Values
@@ -92,6 +88,7 @@ View the full list of Codegen "config option" parameters with the command:
java -jar swagger-codegen-cli.jar config-help -l haskell-http-client
```
## Usage Notes
### Example SwaggerPetstore Haddock documentation
@@ -103,12 +100,12 @@ An example of the generated haddock documentation targeting the server http://pe
An example application using the auto-generated haskell-http-client bindings for the server http://petstore.swagger.io/ can be found [here][3]
[3]: https://github.com/swagger-api/swagger-codegen/tree/c7d145a4ba3c0627e04ece9eb97e354ac91be821/samples/client/petstore/haskell-http-client/example-app
### Usage Notes
[3]: https://github.com/swagger-api/swagger-codegen/tree/master/samples/client/petstore/haskell-http-client/example-app
This library is intended to be imported qualified.
### Modules
| MODULE | NOTES |
| ------------------- | --------------------------------------------------- |
| {{title}}.Client | use the "dispatch" functions to send requests |
@@ -118,6 +115,9 @@ This library is intended to be imported qualified.
| {{title}}.Lens | lenses for model fields |
| {{title}}.Logging | logging functions and utils |
### MimeTypes
This library adds type safety around what swagger specifies as
Produces and Consumes for each Operation (e.g. the list of MIME types an
Operation can Produce (using 'accept' headers) and Consume (using 'content-type' headers).
@@ -151,16 +151,6 @@ this would indicate that:
* the _addFoo_ operation can set it's body param of _FooModel_ via `setBodyParam`
* the _addFoo_ operation can set 2 different optional parameters via `applyOptionalParam`
putting this together:
```haskell
let addFooRequest = addFoo MimeJSON foomodel requiredparam1 requiredparam2
`applyOptionalParam` FooId 1
`applyOptionalParam` FooName "name"
`setHeader` [("api_key","xxyy")]
addFooResult <- dispatchMime mgr config addFooRequest MimeXML
```
If the swagger spec doesn't declare it can accept or produce a certain
MIME type for a given Operation, you should either add a Produces or
Consumes instance for the desired MIME types (assuming the server
@@ -174,4 +164,29 @@ x-www-form-urlencoded instances (FromFrom, ToForm) will also be
generated if the model fields are primitive types, and there are
Operations using x-www-form-urlencoded which use those models.
See the example app and the haddocks for details.
### Authentication
A haskell data type will be generated for each swagger authentication type.
If for example the AuthMethod `AuthOAuthFoo` is generated for OAuth operations, then
`addAuthMethod` should be used to add the AuthMethod config.
When a request is dispatched, if a matching auth method is found in
the config, it will be applied to the request.
### Example
```haskell
mgr <- newManager defaultManagerSettings
config0 <- withStdoutLogging =<< newConfig
let config = config0
`addAuthMethod` AuthOAuthFoo "secret-key"
let addFooRequest = addFoo MimeJSON foomodel requiredparam1 requiredparam2
`applyOptionalParam` FooId 1
`applyOptionalParam` FooName "name"
`setHeader` [("qux_header","xxyy")]
addFooResult <- dispatchMime mgr config addFooRequest MimeXML
```
See the example app and the haddocks for details.

View File

@@ -94,6 +94,7 @@ test-suite tests
, time
, iso8601-time
, aeson
, vector
, semigroups
, QuickCheck
other-modules:

View File

@@ -2,20 +2,23 @@
module Instances where
import Data.Text (Text, pack)
import Control.Monad
import Data.Char (isSpace)
import Data.List (sort)
import qualified Data.Time as TI
import Test.QuickCheck
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as Set
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Time as TI
import qualified Data.Vector as V
import ApproxEq
import {{title}}.Model
instance Arbitrary Text where
arbitrary = pack <$> arbitrary
instance Arbitrary T.Text where
arbitrary = T.pack <$> arbitrary
instance Arbitrary TI.Day where
arbitrary = TI.ModifiedJulianDay . (2000 +) <$> arbitrary
@@ -45,6 +48,27 @@ instance Arbitrary Date where
arbitrary = Date <$> arbitrary
shrink (Date xs) = Date <$> shrink xs
-- | A naive Arbitrary instance for A.Value:
instance Arbitrary A.Value where
arbitrary = frequency [(3, simpleTypes), (1, arrayTypes), (1, objectTypes)]
where
simpleTypes :: Gen A.Value
simpleTypes =
frequency
[ (1, return A.Null)
, (2, liftM A.Bool (arbitrary :: Gen Bool))
, (2, liftM (A.Number . fromIntegral) (arbitrary :: Gen Int))
, (2, liftM (A.String . T.pack) (arbitrary :: Gen String))
]
mapF (k, v) = (T.pack k, v)
simpleAndArrays = frequency [(1, sized sizedArray), (4, simpleTypes)]
arrayTypes = sized sizedArray
objectTypes = sized sizedObject
sizedArray n = liftM (A.Array . V.fromList) $ replicateM n simpleTypes
sizedObject n =
liftM (A.object . map mapF) $
replicateM n $ (,) <$> (arbitrary :: Gen String) <*> simpleAndArrays
-- | Checks if a given list has no duplicates in _O(n log n)_.
hasNoDups
:: (Ord a)

View File

@@ -17,7 +17,8 @@ import {{title}}.MimeTypes
main :: IO ()
main =
hspec $ modifyMaxSize (const 10) $
do describe "JSON instances" $
do {{#models}}{{#model}}propMimeEq MimeJSON (Proxy :: Proxy {{classname}})
{{/model}}{{/models}}
hspec $ modifyMaxSize (const 10) $ do
describe "JSON instances" $ do
pure ()
{{#models}}{{#model}}propMimeEq MimeJSON (Proxy :: Proxy {{classname}})
{{/model}}{{/models}}