[scala] fix akka-scala-client template compilation warnings (#5106)

* fix akka-scala-client template compilation warnings

add COOKIE location authorization key
regenerate template

* fix scala akka-http client model imports

* fix #4004 custom package name in reference.conf

* fix scaka-akka codegen test
This commit is contained in:
Aleksandr Nekrasov 2020-02-06 21:15:43 +07:00 committed by GitHub
parent 8779fc6485
commit 1bba3a563e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 202 additions and 104 deletions

View File

@ -150,12 +150,13 @@ public class ScalaAkkaClientCodegen extends AbstractScalaCodegen implements Code
super.processOpts();
if (additionalProperties.containsKey("mainPackage")) {
setMainPackage((String) additionalProperties.get("mainPackage"));
additionalProperties.replace("configKeyPath", this.configKeyPath);
apiPackage = mainPackage + ".api";
modelPackage = mainPackage + ".model";
invokerPackage = mainPackage + ".core";
additionalProperties.put("apiPackage", apiPackage);
additionalProperties.put("modelPackage", apiPackage);
additionalProperties.put("invokerPackage", apiPackage);
additionalProperties.put("modelPackage", modelPackage);
additionalProperties.put("invokerPackage", invokerPackage);
}
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
@ -366,6 +367,6 @@ public class ScalaAkkaClientCodegen extends AbstractScalaCodegen implements Code
}
public void setMainPackage(String mainPackage) {
this.mainPackage = mainPackage;
this.configKeyPath = this.mainPackage = mainPackage;
}
}

View File

@ -22,7 +22,7 @@ class {{classname}}(baseUrl: String) {
{{/javadocRenderer}}
def {{operationId}}({{>methodParameters}}): ApiRequest[{{>operationReturnType}}] =
ApiRequest[{{>operationReturnType}}](ApiMethods.{{httpMethod.toUpperCase}}, baseUrl, "{{{path}}}", {{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}})
{{#authMethods}}{{#isApiKey}}.withApiKey(apiKey, "{{keyParamName}}", {{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}})
{{#authMethods}}{{#isApiKey}}.withApiKey(apiKey, "{{keyParamName}}", {{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}})
{{/isApiKey}}{{#isBasic}}.withCredentials(basicAuth)
{{/isBasic}}{{/authMethods}}{{#bodyParam}}.withBody({{paramName}})
{{/bodyParam}}{{#formParams}}.withFormParam({{>paramCreation}})

View File

@ -45,7 +45,7 @@ object ApiInvoker {
*
* @param request the apiRequest to be executed
*/
implicit class ApiRequestImprovements[T](request: ApiRequest[T]) {
implicit class ApiRequestImprovements[T: Manifest](request: ApiRequest[T]) {
def response(invoker: ApiInvoker)(implicit ec: ExecutionContext, system: ActorSystem): Future[ApiResponse[T]] =
response(ec, system, invoker)
@ -67,7 +67,7 @@ object ApiInvoker {
def toAkkaHttpMethod: HttpMethod = HttpMethods.getForKey(method.value).getOrElse(HttpMethods.GET)
}
case object DateTimeSerializer extends CustomSerializer[DateTime](format => ( {
case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( {
case JString(s) =>
ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s)
}, {
@ -215,7 +215,7 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
Uri(r.basePath + opPathWithParams).withQuery(query)
}
def execute[T](r: ApiRequest[T]): Future[ApiResponse[T]] = {
def execute[T: Manifest](r: ApiRequest[T]): Future[ApiResponse[T]] = {
implicit val timeout: Timeout = settings.connectionTimeout
val request = createRequest(makeUri(r), r)
@ -239,8 +239,8 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
.flatMap(unmarshallApiResponse(r))
}
def unmarshallApiResponse[T](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V) = {
def unmarshallApiResponse[T: Manifest](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V): ApiResponse[V] = {
state match {
case ResponseState.Success =>
ApiResponse(response.status.intValue, value, response.headers.map(header => (header.name, header.value)).toMap)
@ -253,31 +253,29 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
)
}
}
val mf = implicitly(manifest[T])
request
.responseForCode(response.status.intValue)
.map {
case (Manifest.Unit, state: ResponseState) =>
// FIXME Casting is ugly, how to do better?
Future(responseForState(state, Unit)).asInstanceOf[Future[ApiResponse[T]]]
case (manifest: Manifest[T], state: ResponseState) =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](manifest, serialization, formats)
.responseForCode(response.status.intValue) match {
case Some((Manifest.Unit, state: ResponseState)) =>
Future(responseForState(state, Unit).asInstanceOf[ApiResponse[T]])
case Some((manifest, state: ResponseState)) if manifest == mf =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](mf, serialization, formats)
Unmarshal(response.entity)
.to[T]
.recoverWith {
case e ⇒ throw ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString), e)
}
.map(value => responseForState(state, value))
case None | Some(_) =>
Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString)))
}
.getOrElse(Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString))))
}
}
sealed trait CustomContentTypes {
protected def normalizedContentType(original: String): ContentType =
ContentType(MediaTypes.forExtension(original), () => HttpCharsets.`UTF-8`)
ContentType(parseContentType(original).mediaType, () => HttpCharsets.`UTF-8`)
protected def parseContentType(contentType: String): ContentType = {

View File

@ -1,9 +1,10 @@
{{>licenseInfo}}
package {{package}}
{{#imports}}
import {{import}}
{{/imports}}
import {{mainPackage}}.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
{{#models}}
{{#model}}

View File

@ -85,6 +85,8 @@ object ApiKeyLocations {
case object HEADER extends ApiKeyLocation
case object COOKIE extends ApiKeyLocation
}

View File

@ -26,6 +26,15 @@ paths:
description: ''
operationId: addPet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
@ -41,6 +50,15 @@ paths:
description: ''
operationId: updatePet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':

View File

@ -11,9 +11,8 @@
*/
package hello.world.model
import hello.world.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
import hello.world.core.ApiModel
case class SomeObj (
`type`: Option[SomeObjEnums.`Type`] = None,

View File

@ -91,6 +91,8 @@ Class | Method | HTTP request | Description
- [ApiResponse](ApiResponse.md)
- [Category](Category.md)
- [InlineObject](InlineObject.md)
- [InlineObject1](InlineObject1.md)
- [Order](Order.md)
- [Pet](Pet.md)
- [Tag](Tag.md)
@ -106,6 +108,12 @@ Authentication schemes defined for the API:
- **API key parameter name**: api_key
- **Location**: HTTP header
### auth_cookie
- **Type**: API key
- **API key parameter name**: AUTH_KEY
- **Location**:
## Author

View File

@ -1 +1 @@
sbt.version=1.2.8
sbt.version=1.3.6

View File

@ -27,13 +27,15 @@ class PetApi(baseUrl: String) {
/**
* Expected answers:
* code 200 : Pet (successful operation)
* code 405 : (Invalid input)
*
* @param body Pet object that needs to be added to the store
* @param pet Pet object that needs to be added to the store
*/
def addPet(body: Pet): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/pet", "application/json")
.withBody(body)
def addPet(pet: Pet): ApiRequest[Pet] =
ApiRequest[Pet](ApiMethods.POST, baseUrl, "/pet", "application/json")
.withBody(pet)
.withSuccessResponse[Pet](200)
.withErrorResponse[Unit](405)
@ -107,15 +109,17 @@ class PetApi(baseUrl: String) {
/**
* Expected answers:
* code 200 : Pet (successful operation)
* code 400 : (Invalid ID supplied)
* code 404 : (Pet not found)
* code 405 : (Validation exception)
*
* @param body Pet object that needs to be added to the store
* @param pet Pet object that needs to be added to the store
*/
def updatePet(body: Pet): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.PUT, baseUrl, "/pet", "application/json")
.withBody(body)
def updatePet(pet: Pet): ApiRequest[Pet] =
ApiRequest[Pet](ApiMethods.PUT, baseUrl, "/pet", "application/json")
.withBody(pet)
.withSuccessResponse[Pet](200)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)
.withErrorResponse[Unit](405)

View File

@ -77,11 +77,11 @@ class StoreApi(baseUrl: String) {
* code 200 : Order (successful operation)
* code 400 : (Invalid Order)
*
* @param body order placed for purchasing the pet
* @param order order placed for purchasing the pet
*/
def placeOrder(body: Order): ApiRequest[Order] =
def placeOrder(order: Order): ApiRequest[Order] =
ApiRequest[Order](ApiMethods.POST, baseUrl, "/store/order", "application/json")
.withBody(body)
.withBody(order)
.withSuccessResponse[Order](200)
.withErrorResponse[Unit](400)

View File

@ -29,11 +29,15 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 0 : (successful operation)
*
* @param body Created user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user Created user object
*/
def createUser(body: User): ApiRequest[Unit] =
def createUser(user: User)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]
@ -41,11 +45,15 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 0 : (successful operation)
*
* @param body List of user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user List of user object
*/
def createUsersWithArrayInput(body: Seq[User]): ApiRequest[Unit] =
def createUsersWithArrayInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user/createWithArray", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]
@ -53,11 +61,15 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 0 : (successful operation)
*
* @param body List of user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user List of user object
*/
def createUsersWithListInput(body: Seq[User]): ApiRequest[Unit] =
def createUsersWithListInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user/createWithList", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]
@ -68,10 +80,14 @@ class UserApi(baseUrl: String) {
* code 400 : (Invalid username supplied)
* code 404 : (User not found)
*
* Available security schemes:
* auth_cookie (apiKey)
*
* @param username The name that needs to be deleted
*/
def deleteUser(username: String): ApiRequest[Unit] =
def deleteUser(username: String)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.DELETE, baseUrl, "/user/{username}", "application/json")
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withPathParam("username", username)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)
@ -97,6 +113,7 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 200 : String (successful operation)
* Headers :
* Set-Cookie - Cookie authentication key for use with the `auth_cookie` apiKey authentication.
* X-Rate-Limit - calls per hour allowed by the user
* X-Expires-After - date in UTC when toekn expires
* code 400 : (Invalid username/password supplied)
@ -112,6 +129,7 @@ class UserApi(baseUrl: String) {
.withErrorResponse[Unit](400)
object LoginUserHeaders {
def setCookie(r: ApiReturnWithHeaders) = r.getStringHeader("Set-Cookie")
def xRateLimit(r: ApiReturnWithHeaders) = r.getIntHeader("X-Rate-Limit")
def xExpiresAfter(r: ApiReturnWithHeaders) = r.getDateTimeHeader("X-Expires-After")
}
@ -119,9 +137,13 @@ class UserApi(baseUrl: String) {
/**
* Expected answers:
* code 0 : (successful operation)
*
* Available security schemes:
* auth_cookie (apiKey)
*/
def logoutUser(): ApiRequest[Unit] =
def logoutUser()(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.GET, baseUrl, "/user/logout", "application/json")
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withDefaultSuccessResponse[Unit]
@ -132,12 +154,16 @@ class UserApi(baseUrl: String) {
* code 400 : (Invalid user supplied)
* code 404 : (User not found)
*
* Available security schemes:
* auth_cookie (apiKey)
*
* @param username name that need to be deleted
* @param body Updated user object
* @param user Updated user object
*/
def updateUser(username: String, body: User): ApiRequest[Unit] =
def updateUser(username: String, user: User)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.PUT, baseUrl, "/user/{username}", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withPathParam("username", username)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)

View File

@ -55,7 +55,7 @@ object ApiInvoker {
*
* @param request the apiRequest to be executed
*/
implicit class ApiRequestImprovements[T](request: ApiRequest[T]) {
implicit class ApiRequestImprovements[T: Manifest](request: ApiRequest[T]) {
def response(invoker: ApiInvoker)(implicit ec: ExecutionContext, system: ActorSystem): Future[ApiResponse[T]] =
response(ec, system, invoker)
@ -77,7 +77,7 @@ object ApiInvoker {
def toAkkaHttpMethod: HttpMethod = HttpMethods.getForKey(method.value).getOrElse(HttpMethods.GET)
}
case object DateTimeSerializer extends CustomSerializer[DateTime](format => ( {
case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( {
case JString(s) =>
ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s)
}, {
@ -225,7 +225,7 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
Uri(r.basePath + opPathWithParams).withQuery(query)
}
def execute[T](r: ApiRequest[T]): Future[ApiResponse[T]] = {
def execute[T: Manifest](r: ApiRequest[T]): Future[ApiResponse[T]] = {
implicit val timeout: Timeout = settings.connectionTimeout
val request = createRequest(makeUri(r), r)
@ -249,8 +249,8 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
.flatMap(unmarshallApiResponse(r))
}
def unmarshallApiResponse[T](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V) = {
def unmarshallApiResponse[T: Manifest](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V): ApiResponse[V] = {
state match {
case ResponseState.Success =>
ApiResponse(response.status.intValue, value, response.headers.map(header => (header.name, header.value)).toMap)
@ -263,31 +263,29 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
)
}
}
val mf = implicitly(manifest[T])
request
.responseForCode(response.status.intValue)
.map {
case (Manifest.Unit, state: ResponseState) =>
// FIXME Casting is ugly, how to do better?
Future(responseForState(state, Unit)).asInstanceOf[Future[ApiResponse[T]]]
case (manifest: Manifest[T], state: ResponseState) =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](manifest, serialization, formats)
.responseForCode(response.status.intValue) match {
case Some((Manifest.Unit, state: ResponseState)) =>
Future(responseForState(state, Unit).asInstanceOf[ApiResponse[T]])
case Some((manifest, state: ResponseState)) if manifest == mf =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](mf, serialization, formats)
Unmarshal(response.entity)
.to[T]
.recoverWith {
case e throw ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString), e)
}
.map(value => responseForState(state, value))
case None | Some(_) =>
Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString)))
}
.getOrElse(Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString))))
}
}
sealed trait CustomContentTypes {
protected def normalizedContentType(original: String): ContentType =
ContentType(MediaTypes.forExtension(original), () => HttpCharsets.`UTF-8`)
ContentType(parseContentType(original).mediaType, () => HttpCharsets.`UTF-8`)
protected def parseContentType(contentType: String): ContentType = {

View File

@ -95,6 +95,8 @@ object ApiKeyLocations {
case object HEADER extends ApiKeyLocation
case object COOKIE extends ApiKeyLocation
}

View File

@ -12,8 +12,6 @@
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
case class ApiResponse (
code: Option[Int] = None,

View File

@ -12,8 +12,6 @@
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
case class Category (
id: Option[Long] = None,

View File

@ -0,0 +1,23 @@
/**
* OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
case class InlineObject (
/* Updated name of the pet */
name: Option[String] = None,
/* Updated status of the pet */
status: Option[String] = None
) extends ApiModel

View File

@ -0,0 +1,24 @@
/**
* OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package org.openapitools.client.model
import java.io.File
import org.openapitools.client.core.ApiModel
case class InlineObject1 (
/* Additional data to pass to server */
additionalMetadata: Option[String] = None,
/* file to upload */
file: Option[File] = None
) extends ApiModel

View File

@ -11,9 +11,8 @@
*/
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
import org.openapitools.client.core.ApiModel
case class Order (
id: Option[Long] = None,

View File

@ -12,8 +12,6 @@
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
case class Pet (
id: Option[Long] = None,

View File

@ -12,8 +12,6 @@
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
case class Tag (
id: Option[Long] = None,

View File

@ -12,8 +12,6 @@
package org.openapitools.client.model
import org.openapitools.client.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
case class User (
id: Option[Long] = None,

View File

@ -14,8 +14,8 @@ class PetApiTest extends AsyncFlatSpec with Matchers {
private implicit val system: ActorSystem = ActorSystem()
behavior of "PetApi"
val api = PetApi()
val invoker = ApiInvoker()
val api: PetApi = PetApi()
val invoker: ApiInvoker = ApiInvoker(EnumsSerializers.all)
private implicit val apiKey: ApiKeyValue = ApiKeyValue("special-key")
it should "add and fetch a pet" in {
@ -54,7 +54,7 @@ class PetApiTest extends AsyncFlatSpec with Matchers {
}
it should "update a pet" in {
val petId = Math.random().toInt
val petId = (Math.random()*1000000000).toLong
val createdPet = Pet(
Some(petId),
Some(Category(Some(1), Some("sold"))),
@ -65,18 +65,23 @@ class PetApiTest extends AsyncFlatSpec with Matchers {
)
for {
_ <- invoker.execute(api.addPet(createdPet))
pet: core.ApiResponse[Pet] <- invoker.execute(api.getPetById(petId))
updatedPet = pet.content.copy(status = Some(PetEnums.Status.Sold))
_ <- invoker.execute(api.updatePet(updatedPet))
updatedRequested <- invoker.execute(api.getPetById(petId))
createdPet <- invoker.execute(api.addPet(createdPet))
pet: core.ApiResponse[Pet] <- invoker.execute(api.getPetById(createdPet.content.id.get))
updatedPet = pet.content.copy(status = Some(PetEnums.Status.Sold), name = "developer")
updatedPetResponse: core.ApiResponse[Pet] <- invoker.execute(api.updatePet(updatedPet))
updatedRequested: core.ApiResponse[Pet] <- invoker.execute(api.getPetById(createdPet.content.id.get))
} yield {
pet.content.name should be("programmer")
pet.content.status should be(Some(PetEnums.Status.Available))
updatedRequested.content.name should be("programmer")
updatedPetResponse.content.name should be("developer")
updatedPetResponse.content.status should be(Some(PetEnums.Status.Sold))
updatedRequested.content.name should be("developer")
updatedRequested.content.status should be(Some(PetEnums.Status.Sold))
}
}
it should "find pets by status" in {
@ -90,7 +95,7 @@ class PetApiTest extends AsyncFlatSpec with Matchers {
pets should not be empty
forAll(pets) { pet =>
pet.status should contain("available")
pet.status should contain(PetEnums.Status.Available)
}
}
}