<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Validation\Spec;

use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\DocumentValidatorInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidArgumentException;
use CloudCreativity\LaravelJsonApi\Object\ResourceIdentifier;
use CloudCreativity\LaravelJsonApi\Validation\ErrorTranslator;
use Neomerx\JsonApi\Exceptions\ErrorCollection;

/**
 * Class AbstractValidator
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractValidator implements DocumentValidatorInterface
{

    /**
     * @var object
     */
    protected $document;

    /**
     * @var StoreInterface
     */
    protected $store;

    /**
     * @var ErrorTranslator
     */
    protected $translator;

    /**
     * @var ErrorCollection
     */
    protected $errors;

    /**
     * @var bool|null
     */
    private $valid;

    /**
     * @return bool
     */
    abstract protected function validate(): bool;

    /**
     * AbstractValidator constructor.
     *
     * @param StoreInterface $store
     * @param ErrorTranslator $translator
     * @param object $document
     */
    public function __construct(StoreInterface $store, ErrorTranslator $translator, $document)
    {
        if (!is_object($document)) {
            throw new InvalidArgumentException('Expecting JSON API document to be an object.');
        }

        $this->store = $store;
        $this->document = $document;
        $this->translator = $translator;
        $this->errors = new ErrorCollection();
    }

    /**
     * @inheritDoc
     */
    public function fails(): bool
    {
        return !$this->passes();
    }

    /**
     * @return bool
     */
    public function passes(): bool
    {
        if (is_bool($this->valid)) {
            return $this->valid;
        }

        return $this->valid = $this->validate();
    }

    /**
     * @inheritDoc
     */
    public function getDocument()
    {
        return clone $this->document;
    }

    /**
     * @inheritDoc
     */
    public function getErrors(): ErrorCollection
    {
        return clone $this->errors;
    }

    /**
     * Validate the value of a type member.
     *
     * @param mixed $value
     * @param string $path
     * @return bool
     */
    protected function validateTypeMember($value, string $path): bool
    {
        if (!is_string($value)) {
            $this->memberNotString($path, 'type');
            return false;
        }

        if (empty($value)) {
            $this->memberEmpty($path, 'type');
            return false;
        }

        if (!$this->store->isType($value)) {
            $this->resourceTypeNotRecognised($value, $path);
            return false;
        }

        return true;
    }

    /**
     * Validate the value of an id member.
     *
     * @param mixed $value
     * @param string $path
     * @return bool
     */
    protected function validateIdMember($value, string $path): bool
    {
        if (!is_string($value)) {
            $this->memberNotString($path, 'id');
            return false;
        }

        if (empty($value)) {
            $this->memberEmpty($path, 'id');
            return false;
        }

        return true;
    }

    /**
     * Validate an identifier object.
     *
     * @param mixed $value
     * @param string $path
     *      the path to the data member in which the identifier is contained.
     * @param int|null $index
     *      the index for the identifier, if in a collection.
     * @return bool
     */
    protected function validateIdentifier($value, string $path, ?int $index = null): bool
    {
        $member = is_int($index) ? (string) $index : 'data';

        if (!is_object($value)) {
            $this->memberNotObject($path, $member);
            return false;
        }

        $dataPath = sprintf('%s/%s', rtrim($path, '/'), $member);
        $valid = true;

        if (!property_exists($value, 'type')) {
            $this->memberRequired($dataPath, 'type');
            $valid = false;
        } else if (!$this->validateTypeMember($value->type, $dataPath)) {
            $valid = false;
        }

        if (!property_exists($value, 'id')) {
            $this->memberRequired($dataPath, 'id');
            $valid = false;
        } else if (!$this->validateIdMember($value->id, $dataPath)) {
            $valid = false;
        }

        /** If it has attributes or relationships, it is a resource object not a resource identifier */
        if (property_exists($value, 'attributes') || property_exists($value, 'relationships')) {
            $this->memberNotIdentifier($path, $member);
            $valid = false;
        }

        return $valid;
    }

    /**
     * Validate a resource relationship.
     *
     * @param mixed $relation
     * @param string|null $field
     * @return bool
     */
    protected function validateRelationship($relation, ?string $field = null): bool
    {
        $path = $field ? '/data/relationships' : '/';
        $member = $field ?: 'data';

        if (!is_object($relation)) {
            $this->memberNotObject($path, $member);
            return false;
        }

        $path = $field ? "{$path}/{$field}" : $path;

        if (!property_exists($relation, 'data')) {
            $this->memberRequired($path, 'data');
            return false;
        }

        $data = $relation->data;

        if (is_array($data)) {
            return $this->validateToMany($data, $field);
        }

        return $this->validateToOne($data, $field);
    }

    /**
     * Validate a to-one relation.
     *
     * @param mixed $value
     * @param string|null $field
     *      the relationship field name.
     * @return bool
     */
    protected function validateToOne($value, ?string $field = null): bool
    {
        if (is_null($value)) {
            return true;
        }

        $dataPath = $field ? "/data/relationships/{$field}" : "/";
        $identifierPath = $field ? "/data/relationships/{$field}" : "/data";

        if (!$this->validateIdentifier($value, $dataPath)) {
            return false;
        }

        if (!$this->store->exists(new ResourceIdentifier($value))) {
            $this->resourceDoesNotExist($identifierPath);
            return false;
        }

        return true;
    }

    /**
     * Validate a to-many relation.
     *
     * @param array $value
     * @param string|null $field
     *      the relationship field name.
     * @return bool
     */
    protected function validateToMany(array $value, ?string $field = null): bool
    {
        $path = $field ? "/data/relationships/{$field}/data" : "/data";
        $valid = true;

        foreach ($value as $index => $item) {
            if (!$this->validateIdentifier($item, $path, $index)) {
                $valid = false;
                continue;
            }

            if ($this->isNotFound($item->type, $item->id)) {
                $this->resourceDoesNotExist("{$path}/{$index}");
                $valid = false;
            }
        }

        return $valid;
    }

    /**
     * Does the key exist in document data object?
     *
     * @param $key
     * @return bool
     */
    protected function dataHas($key): bool
    {
        if (!isset($this->document->data)) {
            return false;
        }

        return property_exists($this->document->data, $key);
    }

    /**
     * Get a value from the document data object use dot notation.
     *
     * @param string|array $key
     * @param mixed $default
     * @return mixed|null
     */
    protected function dataGet($key, $default = null)
    {
        if (!isset($this->document->data)) {
            return $default;
        }

        return data_get($this->document->data, $key, $default);
    }

    /**
     * Check if the resource is not found.
     *
     * @param string $type
     * @param string $id
     * @return bool
     */
    protected function isNotFound(string $type, string $id): bool
    {
        return !$this->store->exists(ResourceIdentifier::create($type, $id));
    }

    /**
     * Add an error for a member that is required.
     *
     * @param string $path
     * @param string $member
     * @return void
     */
    protected function memberRequired(string $path, string $member): void
    {
        $this->errors->add($this->translator->memberRequired($path, $member));
    }

    /**
     * Add an error for a member that must be an object.
     *
     * @param string $path
     * @param string $member
     * @return void
     */
    protected function memberNotObject(string $path, string $member): void
    {
        $this->errors->add($this->translator->memberNotObject($path, $member));
    }

    /**
     * Add an error for a member that must be an object.
     *
     * @param string $path
     * @param string|null $member
     * @return void
     */
    protected function memberNotIdentifier(string $path, string $member): void
    {
        $this->errors->add($this->translator->memberNotIdentifier($path, $member));
    }

    /**
     * Add errors for when a member has a field that is not allowed.
     *
     * @param string $path
     * @param string $member
     * @param iterable $fields
     */
    protected function memberFieldsNotAllowed(string $path, string $member, iterable $fields): void
    {
        foreach ($fields as $field) {
            $this->errors->add($this->translator->memberFieldNotAllowed($path, $member, $field));
        }
    }

    /**
     * Add an error for a member that must be a string.
     *
     * @param string $path
     * @param string $member
     * @return void
     */
    protected function memberNotString(string $path, string $member): void
    {
        $this->errors->add($this->translator->memberNotString($path, $member));
    }

    /**
     * Add an error for a member that cannot be an empty value.
     *
     * @param string $path
     * @param string $member
     * @return void
     */
    protected function memberEmpty(string $path, string $member): void
    {
        $this->errors->add($this->translator->memberEmpty($path, $member));
    }

    /**
     * Add an error for when the resource type is not supported by the endpoint.
     *
     * @param string $actual
     * @param string $path
     * @return void
     */
    protected function resourceTypeNotSupported(string $actual, string $path = '/data'): void
    {
        $this->errors->add($this->translator->resourceTypeNotSupported($actual, $path));
    }

    /**
     * Add an error for when a resource type is not recognised.
     *
     * @param string $actual
     * @param string $path
     * @return void
     */
    protected function resourceTypeNotRecognised(string $actual, string $path = '/data'): void
    {
        $this->errors->add($this->translator->resourceTypeNotRecognised($actual, $path));
    }

    /**
     * Add an error for when the resource id is not supported by the endpoint.
     *
     * @param string $actual
     * @param string $path
     * @return void
     */
    protected function resourceIdNotSupported(string $actual, string $path = '/data'): void
    {
        $this->errors->add($this->translator->resourceIdNotSupported($actual, $path));
    }

    /**
     * Add an error for when the resource does not support client-generated ids.
     *
     * @param string $type
     * @param string $path
     * @return void
     */
    protected function resourceDoesNotSupportClientIds(string $type, string $path = '/data'): void
    {
        $this->errors->add($this->translator->resourceDoesNotSupportClientIds($type, $path));
    }

    /**
     * Add an error for when a resource already exists.
     *
     * @param string $type
     * @param string $id
     * @param string $path
     * @return void
     */
    protected function resourceExists(string $type, string $id, string $path = '/data'): void
    {
        $this->errors->add($this->translator->resourceExists($type, $id, $path));
    }

    /**
     * Add an error for when a resource identifier does not exist.
     *
     * @param string $path
     * @return void
     */
    protected function resourceDoesNotExist(string $path): void
    {
        $this->errors->add($this->translator->resourceDoesNotExist($path));
    }

    /**
     * Add errors for when a resource field exists in both the attributes and relationships members.
     *
     * @param iterable $fields
     * @return void
     */
    protected function resourceFieldsExistInAttributesAndRelationships(iterable $fields): void
    {
        foreach ($fields as $field) {
            $this->errors->add(
                $this->translator->resourceFieldExistsInAttributesAndRelationships($field)
            );
        }
    }

}


<?php

/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Exceptions;

use CloudCreativity\LaravelJsonApi\Routing\Route;
use CloudCreativity\LaravelJsonApi\Services\JsonApiService;
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Exceptions\JsonApiException;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * Trait HandlesErrors
 *
 * @package CloudCreativity\LaravelJsonApi
 */
trait HandlesErrors
{

    /**
     * Does the HTTP request require a JSON API error response?
     *
     * This method determines if we need to render a JSON API error response
     * for the client. We need to do this if the client has requested JSON
     * API via its Accept header.
     *
     * @param Request $request
     * @param Exception $e
     * @return bool
     */
    public function isJsonApi($request, Exception $e)
    {
        if (Helpers::wantsJsonApi($request)) {
            return true;
        }

        /** @var Route $route */
        $route = app(JsonApiService::class)->currentRoute();

        return $route->hasCodec() && $route->getCodec()->willEncode();
    }

    /**
     * Render an exception as a JSON API error response.
     *
     * @param Request $request
     * @param Exception $e
     * @return Response
     */
    public function renderJsonApi($request, Exception $e)
    {
        return json_api()->response()->exception($e);
    }

    /**
     * Prepare JSON API exception for non-JSON API rendering.
     *
     * @param JsonApiException $ex
     * @return HttpException
     */
    protected function prepareJsonApiException(JsonApiException $ex)
    {
        $error = collect($ex->getErrors())->map(function (ErrorInterface $err) {
            return $err->getDetail() ?: $err->getTitle();
        })->filter()->first();

        return new HttpException($ex->getHttpCode(), $error, $ex);
    }

}


<?php

/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Http\Responses;

use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Codec\Codec;
use CloudCreativity\LaravelJsonApi\Codec\Encoding;
use CloudCreativity\LaravelJsonApi\Contracts\Exceptions\ExceptionParserInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Http\Responses\ErrorResponseInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PageInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Document\Error;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Http\Response;
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Exceptions\ErrorCollection;
use Neomerx\JsonApi\Http\Responses as BaseResponses;

/**
 * Class Responses
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Responses extends BaseResponses
{

    /**
     * @var Factory
     */
    private $factory;

    /**
     * @var Api
     */
    private $api;

    /**
     * @var Route
     */
    private $route;

    /**
     * @var ExceptionParserInterface
     */
    private $exceptions;

    /**
     * @var Codec|null
     */
    private $codec;

    /**
     * @var EncodingParametersInterface|null
     */
    private $parameters;

    /**
     * Responses constructor.
     *
     * @param Factory $factory
     * @param Api $api
     *      the API that is sending the responses.
     * @param Route $route
     * @param $exceptions
     */
    public function __construct(
        Factory $factory,
        Api $api,
        Route $route,
        ExceptionParserInterface $exceptions
    ) {
        $this->factory = $factory;
        $this->api = $api;
        $this->route = $route;
        $this->exceptions = $exceptions;
    }

    /**
     * @param Codec $codec
     * @return Responses
     */
    public function withCodec(Codec $codec): self
    {
        $this->codec = $codec;

        return $this;
    }

    /**
     * Send a response with the supplied media type content.
     *
     * @param string $mediaType
     * @return $this
     */
    public function withMediaType(string $mediaType): self
    {
        if (!$encoding = $this->api->getEncodings()->find($mediaType)) {
            throw new \InvalidArgumentException(
                "Media type {$mediaType} is not valid for API {$this->api->getName()}."
            );
        }

        $codec = $this->factory->createCodec(
            $this->api->getContainer(),
            $encoding,
            null
        );

        return $this->withCodec($codec);
    }

    /**
     * Set the encoding options.
     *
     * @param int $options
     * @param int $depth
     * @param string|null $mediaType
     * @return Responses
     */
    public function withEncoding(
        int $options = 0,
        int $depth = 512,
        string $mediaType = MediaTypeInterface::JSON_API_MEDIA_TYPE
    ) {
        $encoding = Encoding::create(
            $mediaType,
            $options,
            $this->api->getUrl()->toString(),
            $depth
        );

        $codec = $this->factory->createCodec(
            $this->api->getContainer(),
            $encoding,
            null
        );

        return $this->withCodec($codec);
    }

    /**
     * Set the encoding parameters to use.
     *
     * @param EncodingParametersInterface|null $parameters
     * @return $this
     */
    public function withEncodingParameters(?EncodingParametersInterface $parameters): self
    {
        $this->parameters = $parameters;

        return $this;
    }

    /**
     * @param $statusCode
     * @param array $headers
     * @return mixed
     */
    public function statusCode($statusCode, array $headers = [])
    {
        return $this->getCodeResponse($statusCode, $headers);
    }

    /**
     * @param array $headers
     * @return mixed
     */
    public function noContent(array $headers = [])
    {
        return $this->getCodeResponse(204, $headers);
    }

    /**
     * @param $meta
     * @param int $statusCode
     * @param array $headers
     * @return mixed
     */
    public function meta($meta, $statusCode = self::HTTP_OK, array $headers = [])
    {
        return $this->getMetaResponse($meta, $statusCode, $headers);
    }

    /**
     * @param array $links
     * @param $meta
     * @param int $statusCode
     * @param array $headers
     * @return mixed
     */
    public function noData(array $links = [], $meta = null, $statusCode = self::HTTP_OK, array $headers = [])
    {
        $encoder = $this->getEncoder();
        $content = $encoder->withLinks($links)->encodeMeta($meta ?: []);

        return $this->createJsonApiResponse($content, $statusCode, $headers, true);
    }

    /**
     * @param $data
     * @param array $links
     * @param mixed $meta
     * @param int $statusCode
     * @param array $headers
     * @return mixed
     */
    public function content(
        $data,
        array $links = [],
        $meta = null,
        $statusCode = self::HTTP_OK,
        array $headers = []
    ) {
        return $this->getContentResponse($data, $statusCode, $links, $meta, $headers);
    }

    /**
     * @inheritdoc
     */
    public function getContentResponse(
        $data,
        $statusCode = self::HTTP_OK,
        $links = null,
        $meta = null,
        array $headers = []
    ) {
        if ($data instanceof PageInterface) {
            list ($data, $meta, $links) = $this->extractPage($data, $meta, $links);
        }

        return parent::getContentResponse($data, $statusCode, $links, $meta, $headers);
    }

    /**
     * @param $resource
     * @param array $links
     * @param mixed $meta
     * @param array $headers
     * @return mixed
     */
    public function created($resource = null, array $links = [], $meta = null, array $headers = [])
    {
        if ($this->isNoContent($resource, $links, $meta)) {
            return $this->noContent();
        }

        if (is_null($resource)) {
            return $this->noData($links, $meta, self::HTTP_OK, $headers);
        }

        if ($this->isAsync($resource)) {
            return $this->accepted($resource, $links, $meta, $headers);
        }

        return $this->getCreatedResponse($resource, $links, $meta, $headers);
    }

    /**
     * Return a response for a resource update request.
     *
     * @param $resource
     * @param array $links
     * @param mixed $meta
     * @param array $headers
     * @return mixed
     */
    public function updated(
        $resource = null,
        array $links = [],
        $meta = null,
        array $headers = []
    ) {
        return $this->getResourceResponse($resource, $links, $meta, $headers);
    }

    /**
     * Return a response for a resource delete request.
     *
     * @param mixed|null $resource
     * @param array $links
     * @param mixed|null $meta
     * @param array $headers
     * @return mixed
     */
    public function deleted(
        $resource = null,
        array $links = [],
        $meta = null,
        array $headers = []
    ) {
        return $this->getResourceResponse($resource, $links, $meta, $headers);
    }

    /**
     * @param AsynchronousProcess $job
     * @param array $links
     * @param null $meta
     * @param array $headers
     * @return mixed
     */
    public function accepted(AsynchronousProcess $job, array $links = [], $meta = null, array $headers = [])
    {
        $headers['Content-Location'] = $this->getResourceLocationUrl($job);

        return $this->getContentResponse($job, Response::HTTP_ACCEPTED, $links, $meta, $headers);
    }

    /**
     * @param AsynchronousProcess $job
     * @param array $links
     * @param null $meta
     * @param array $headers
     * @return \Illuminate\Http\RedirectResponse|mixed
     */
    public function process(AsynchronousProcess $job, array $links = [], $meta = null, array $headers = [])
    {
        if (!$job->isPending() && $location = $job->getLocation()) {
            $headers['Location'] = $location;
            return $this->createJsonApiResponse(null, Response::HTTP_SEE_OTHER, $headers);
        }

        return $this->getContentResponse($job, self::HTTP_OK, $links, $meta, $headers);
    }

    /**
     * @param $data
     * @param array $links
     * @param mixed $meta
     * @param int $statusCode
     * @param array $headers
     * @return mixed
     */
    public function relationship(
        $data,
        array $links = [],
        $meta = null,
        $statusCode = 200,
        array $headers = []
    ) {
        return $this->getIdentifiersResponse($data, $statusCode, $links, $meta, $headers);
    }

    /**
     * @param array|object $data
     * @param int $statusCode
     * @param $links
     * @param $meta
     * @param array $headers
     * @return mixed
     */
    public function getIdentifiersResponse(
        $data,
        $statusCode = self::HTTP_OK,
        $links = null,
        $meta = null,
        array $headers = []
    ) {
        if ($data instanceof PageInterface) {
            list ($data, $meta, $links) = $this->extractPage($data, $meta, $links);
        }

        return parent::getIdentifiersResponse($data, $statusCode, $links, $meta, $headers);
    }

    /**
     * Create a response containing a single error.
     *
     * @param string|array|ErrorInterface $error
     * @param int|null $defaultStatusCode
     * @param array $headers
     * @return mixed
     */
    public function error($error, $defaultStatusCode = null, array $headers = [])
    {
        if (is_string($error)) {
            $error = $this->api->getErrors()->error($error);
        } else if (is_array($error)) {
            $error = Error::create($error);
        }

        if (!$error instanceof ErrorInterface) {
            throw new \InvalidArgumentException('Expecting a string, array or error object.');
        }

        return $this->errors($error, $defaultStatusCode, $headers);
    }

    /**
     * Create a response containing multiple errors.
     *
     * @param mixed $errors
     * @param int|null $defaultStatusCode
     * @param array $headers
     * @return mixed
     */
    public function errors($errors, $defaultStatusCode = null, array $headers = [])
    {
        if ($errors instanceof ErrorResponseInterface) {
            return $this->getErrorResponse($errors);
        }

        if (is_array($errors)) {
            $errors = $this->api->getErrors()->errors(...$errors);
        }

        return $this->errors(
            $this->factory->createErrorResponse($errors, $defaultStatusCode, $headers)
        );
    }

    /**
     * Render an exception that has arisen from the exception handler.
     *
     * @param \Exception $ex
     * @return mixed
     */
    public function exception(\Exception $ex)
    {
        /** If the current codec cannot encode JSON API, we need to reset it. */
        if ($this->getCodec()->willNotEncode()) {
            $this->codec = $this->api->getDefaultCodec();
        }

        return $this->getErrorResponse(
            $this->exceptions->parse($ex)
        );
    }

    /**
     * @param ErrorInterface|ErrorInterface[]|ErrorCollection|ErrorResponseInterface $errors
     * @param int $statusCode
     * @param array $headers
     * @return mixed
     */
    public function getErrorResponse($errors, $statusCode = self::HTTP_BAD_REQUEST, array $headers = [])
    {
        if ($errors instanceof ErrorResponseInterface) {
            $statusCode = $errors->getHttpCode();
            $headers = $errors->getHeaders();
            $errors= $errors->getErrors();
        }

        return parent::getErrorResponse($errors, $statusCode, $headers);
    }

    /**
     * @param $resource
     * @param array $links
     * @param null $meta
     * @param array $headers
     * @return mixed
     */
    protected function getResourceResponse($resource, array $links = [], $meta = null, array $headers = [])
    {
        if ($this->isNoContent($resource, $links, $meta)) {
            return $this->noContent();
        }

        if (is_null($resource)) {
            return $this->noData($links, $meta, self::HTTP_OK, $headers);
        }

        if ($this->isAsync($resource)) {
            return $this->accepted($resource, $links, $meta, $headers);
        }

        return $this->getContentResponse($resource, self::HTTP_OK, $links, $meta, $headers);
    }

    /**
     * @inheritdoc
     */
    protected function getEncoder()
    {
        return $this->getCodec()->getEncoder();
    }

    /**
     * @inheritdoc
     */
    protected function getMediaType()
    {
        return $this->getCodec()->getEncodingMediaType();
    }

    /**
     * @return Codec
     */
    protected function getCodec()
    {
        if (!$this->codec) {
            $this->codec = $this->getDefaultCodec();
        }

        return $this->codec;
    }

    /**
     * @return Codec
     */
    protected function getDefaultCodec()
    {
        if ($this->route->hasCodec()) {
            return $this->route->getCodec();
        }

        return $this->api->getDefaultCodec();
    }

    /**
     * @inheritdoc
     */
    protected function getUrlPrefix()
    {
        return $this->api->getUrl()->toString();
    }

    /**
     * @inheritdoc
     */
    protected function getEncodingParameters()
    {
        return $this->parameters;
    }

    /**
     * @inheritdoc
     */
    protected function getSchemaContainer()
    {
        return $this->api->getContainer();
    }

    /**
     * @inheritdoc
     */
    protected function getSupportedExtensions()
    {
        return $this->api->getSupportedExtensions();
    }

    /**
     * @inheritdoc
     */
    protected function createResponse($content, $statusCode, array $headers)
    {
        return response($content, $statusCode, $headers);
    }

    /**
     * Does a no content response need to be returned?
     *
     * @param $resource
     * @param $links
     * @param $meta
     * @return bool
     */
    protected function isNoContent($resource, $links, $meta)
    {
        return is_null($resource) && empty($links) && empty($meta);
    }

    /**
     * Does the data represent an asynchronous process?
     *
     * @param $data
     * @return bool
     */
    protected function isAsync($data)
    {
        return $data instanceof AsynchronousProcess;
    }

    /**
     * Reset the encoder.
     *
     * @return void
     */
    protected function resetEncoder()
    {
        $this->getEncoder()->withLinks([])->withMeta(null);
    }

    /**
     * @param PageInterface $page
     * @param $meta
     * @param $links
     * @return array
     */
    private function extractPage(PageInterface $page, $meta, $links)
    {
        return [
            $page->getData(),
            $this->mergePageMeta($meta, $page),
            $this->mergePageLinks($links, $page),
        ];
    }

    /**
     * @param object|array|null $existing
     * @param PageInterface $page
     * @return array
     */
    private function mergePageMeta($existing, PageInterface $page)
    {
        if (!$merge = $page->getMeta()) {
            return $existing;
        }

        $existing = (array) $existing ?: [];

        if ($key = $page->getMetaKey()) {
            $existing[$key] = $merge;
            return $existing;
        }

        return array_replace($existing, (array) $merge);
    }

    /**
     * @param array $existing
     * @param PageInterface $page
     * @return array
     */
    private function mergePageLinks(array $existing, PageInterface $page)
    {
        return array_replace($existing, array_filter([
            DocumentInterface::KEYWORD_FIRST => $page->getFirstLink(),
            DocumentInterface::KEYWORD_PREV => $page->getPreviousLink(),
            DocumentInterface::KEYWORD_NEXT => $page->getNextLink(),
            DocumentInterface::KEYWORD_LAST => $page->getLastLink(),
        ]));
    }

}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Exceptions;

use Illuminate\Support\Collection;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use function CloudCreativity\LaravelJsonApi\json_decode;

/**
 * Class ClientException
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class ClientException extends \RuntimeException
{

    /**
     * @var RequestInterface|null
     */
    private $request;

    /**
     * @var ResponseInterface|null
     */
    private $response;

    /**
     * @var array|null
     */
    private $errors;

    /**
     * ClientException constructor.
     *
     * @param RequestInterface $request
     * @param ResponseInterface|null $response
     * @param \Exception|null $previous
     */
    public function __construct(
        RequestInterface $request,
        ResponseInterface $response = null,
        \Exception $previous = null
    ) {
        parent::__construct(
            $previous ? $previous->getMessage() : 'Client encountered an error.',
            $response ? $response->getStatusCode() : 0,
            $previous
        );

        $this->request = $request;
        $this->response = $response;
    }

    /**
     * @return RequestInterface|null
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * @return ResponseInterface|null
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * @return bool
     */
    public function hasResponse()
    {
        return !is_null($this->response);
    }

    /**
     * Get any JSON API errors that are in the response.
     *
     * @return Collection
     */
    public function getErrors()
    {
        if (!is_null($this->errors)) {
            return collect($this->errors);
        }

        try {
            $this->errors = $this->parse();
        } catch (\Exception $ex) {
            $this->errors = [];
        }

        return collect($this->errors);
    }

    /**
     * @return int|null
     */
    public function getHttpCode()
    {
        return $this->response ? $this->response->getStatusCode() : null;
    }

    /**
     * Parse JSON API errors out of the response body.
     *
     * @return array
     */
    private function parse()
    {
        if (!$this->response) {
            return [];
        }

        $body = json_decode((string) $this->response->getBody(), true);

        return isset($body['errors']) ? $body['errors'] : [];
    }
}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Eloquent;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

/**
 * Class HasMany
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class HasMany extends AbstractManyRelation
{

    /**
     * @param Model $record
     * @param array $relationship
     * @param EncodingParametersInterface $parameters
     * @return void
     */
    public function update($record, array $relationship, EncodingParametersInterface $parameters)
    {
        $related = $this->findRelated($record, $relationship);
        $relation = $this->getRelation($record, $this->key);

        if ($relation instanceof Relations\BelongsToMany) {
            $relation->sync($related);
            return;
        } else {
            $this->sync($relation, $record->{$this->key}, $related);
        }

        // do not refresh as we expect the resource adapter to refresh the record.
    }

    /**
     * @param Model $record
     * @param array $relationship
     * @param EncodingParametersInterface $parameters
     * @return Model
     */
    public function replace($record, array $relationship, EncodingParametersInterface $parameters)
    {
        $this->update($record, $relationship, $parameters);
        $record->refresh(); // in case the relationship has been cached.

        return $record;
    }

    /**
     * Add records to the relationship.
     *
     * Note that the spec says that duplicates MUST NOT be added. The default Laravel
     * behaviour is to add duplicates, therefore we need to do some work to ensure
     * that we only add the records that are not already in the relationship.
     *
     * @param Model $record
     * @param array $relationship
     * @param EncodingParametersInterface $parameters
     * @return Model
     */
    public function add($record, array $relationship, EncodingParametersInterface $parameters)
    {
        $related = $this->findRelated($record, $relationship);
        $relation = $this->getRelation($record, $this->key);

        $existing = $relation
            ->getQuery()
            ->whereKey($related->modelKeys())
            ->get();

        $relation->saveMany($related->diff($existing));
        $record->refresh(); // in case the relationship has been cached.

        return $record;
    }

    /**
     * @param Model $record
     * @param array $relationship
     * @param EncodingParametersInterface $parameters
     * @return Model
     */
    public function remove($record, array $relationship, EncodingParametersInterface $parameters)
    {
        $related = $this->findRelated($record, $relationship);
        $relation = $this->getRelation($record, $this->key);

        if ($relation instanceof Relations\BelongsToMany) {
            $relation->detach($related);
        } else {
            $this->detach($relation, $related);
        }

        $record->refresh(); // in case the relationship has been cached

        return $record;
    }

    /**
     * @inheritdoc
     */
    protected function acceptRelation($relation)
    {
        return $relation instanceof Relations\BelongsToMany ||
            $relation instanceof Relations\HasMany ||
            $relation instanceof Relations\MorphMany;
    }

    /**
     * @param Relations\HasMany|Relations\MorphMany $relation
     * @param Collection $existing
     * @param $updated
     */
    protected function sync($relation, Collection $existing, Collection $updated)
    {
        $add = collect($updated)->reject(function ($model) use ($existing) {
            return $existing->contains($model);
        });

        $remove = $existing->reject(function ($model) use ($updated) {
            return $updated->contains($model);
        });

        if ($remove->isNotEmpty()) {
            $this->detach($relation, $remove);
        }

        $relation->saveMany($add);
    }

    /**
     * @param Relations\HasMany|Relations\MorphMany $relation
     * @param Collection $remove
     */
    protected function detach($relation, Collection $remove)
    {
        /** @var Model $model */
        foreach ($remove as $model) {
            $model->setAttribute($relation->getForeignKeyName(), null)->save();
        }
    }

    /**
     * Find the related models for a JSON API relationship object.
     *
     * We look up the models via the store. These then have to be filtered to
     * ensure they are of the correct model type, because this has-many relation
     * might be used in a polymorphic has-many JSON API relation.
     *
     * @todo this is currently inefficient for polymorphic relationships. We
     * need to be able to filter the resource identifiers by the expected resource
     * type before looking them up via the store.
     *
     * @param $record
     * @param array $relationship
     * @return Collection
     */
    protected function findRelated($record, array $relationship)
    {
        $inverse = $this->getRelation($record, $this->key)->getRelated();
        $related = $this->findToMany($relationship);

        $related = collect($related)->filter(function ($model) use ($inverse) {
            return $model instanceof $inverse;
        });

        return new Collection($related);
    }

}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Eloquent\Concerns;

use Carbon\Carbon;
use Closure;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use DateTime;
use Illuminate\Database\Eloquent\Model;
use Neomerx\JsonApi\Schema\SchemaProvider;

/**
 * Trait SerializesModels
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0
 */
trait SerializesModels
{

    /**
     * Should the created at attribute be included?
     *
     * If the model does not have timestamps, then this setting will be ignored.
     *
     * @var bool
     */
    protected $createdAt = true;

    /**
     * Should the updated at attribute be included?
     *
     * If the model does not have timestamps, then this setting will be ignored.
     *
     * @var bool
     */
    protected $updatedAt = true;

    /**
     * Should the deleted at attribute be included?
     *
     * If the model does not use the `SoftDeletes` trait, this will be ignored.
     *
     * @var bool
     */
    protected $deletedAt = true;

    /**
     * The date format to use.
     *
     * @var string
     */
    protected $dateFormat = Carbon::W3C;

    /**
     * Whether resource field names are hyphenated
     *
     * The JSON API spec recommends using hyphens for resource member names, so this package
     * uses this as the default. If you do not want to follow the recommendation, set this
     * to `false`.
     *
     * @var bool
     */
    protected $hyphenated = true;

    /**
     * The model attribute keys to serialize.
     *
     * - Empty array: no attributes to serialize.
     * - Non-empty array: serialize the specified model keys.
     * - Null: serialize the model's visible keys.
     *
     * @var array|null
     */
    protected $attributes = null;

    /**
     * The model relationships to serialize.
     *
     * @var array
     */
    protected $relationships = [];

    /**
     * @param Model $record
     * @param bool $isPrimary
     * @param array $includedRelationships
     * @return array
     */
    public function getRelationships($record, $isPrimary, array $includedRelationships)
    {
        $relations = [];

        foreach ($this->relationshipKeys($record) as $modelKey => $field) {
            if (is_numeric($modelKey)) {
                $modelKey = $field;
                $field = $this->fieldForRelationship($field);
            }

            $relations[$field] = $this->getRelation($record, $modelKey, $field, $includedRelationships);
        }

        return $relations;
    }

    /**
     * Get attributes that are included for every model class.
     *
     * @param Model $model
     * @return array
     */
    protected function getDefaultAttributes(Model $model)
    {
        $defaults = [];

        if ($this->hasCreatedAtAttribute($model)) {
            $createdAt = $model->getCreatedAtColumn();
            $field = $this->fieldForAttribute($createdAt);
            $defaults[$field] = $this->extractAttribute($model, $createdAt, $field);
        }

        if ($this->hasUpdatedAtAttribute($model)) {
            $updatedAt = $model->getUpdatedAtColumn();
            $field = $this->fieldForAttribute($updatedAt);
            $defaults[$field] = $this->extractAttribute($model, $updatedAt, $field);
        }

        if ($this->hasDeletedAtAttribute($model)) {
            $deletedAt = $model->getDeletedAtColumn();
            $field = $this->fieldForAttribute($deletedAt);
            $defaults[$field] = $this->extractAttribute($model, $deletedAt, $field);
        }

        return $defaults;
    }

    /**
     * Get attributes for the provided model.
     *
     * @param Model $model
     * @return array
     */
    protected function getModelAttributes(Model $model)
    {
        $attributes = [];

        foreach ($this->attributeKeys($model) as $modelKey => $field) {
            if (is_numeric($modelKey)) {
                $modelKey = $field;
                $field = $this->fieldForAttribute($field);
            }

            $attributes[$field] = $this->extractAttribute($model, $modelKey, $field);
        }

        return $attributes;
    }

    /**
     * @param Model $model
     * @param string $modelKey
     * @param string $field
     * @param array $includedRelationships
     * @return array
     */
    protected function getRelation($model, $modelKey, $field, array $includedRelationships)
    {
        return [
            SchemaProvider::SHOW_SELF => true,
            SchemaProvider::SHOW_RELATED => true,
            SchemaProvider::SHOW_DATA => isset($includedRelationships[$field]),
            SchemaProvider::DATA => $this->extractRelationship($model, $modelKey),
        ];
    }

    /**
     * Get the attributes to serialize for the provided model.
     *
     * @param Model $model
     * @return array
     */
    protected function attributeKeys(Model $model)
    {
        if (is_array($this->attributes)) {
            return $this->attributes;
        }

        return $model->getVisible();
    }

    /**
     * @param $model
     * @return array
     */
    protected function relationshipKeys($model)
    {
        return $this->relationships;
    }

    /**
     * Convert a model key into a resource field name.
     *
     * @param $modelKey
     * @return string
     */
    protected function fieldForAttribute($modelKey)
    {
        return $this->hyphenated ? Str::dasherize($modelKey) : $modelKey;
    }

    /**
     * @param $modelKey
     * @return string
     */
    protected function fieldForRelationship($modelKey)
    {
        return $this->fieldForAttribute($modelKey);
    }

    /**
     * @param Model $model
     * @param $modelKey
     * @param $field
     * @return string
     */
    protected function extractAttribute(Model $model, $modelKey, $field)
    {
        $value = $model->{$modelKey};

        return $this->serializeAttribute($value, $model, $modelKey, $field);
    }

    /**
     * @param $model
     * @param $modelKey
     * @return Closure
     */
    protected function extractRelationship($model, $modelKey)
    {
        return function () use ($model, $modelKey) {
            return $model->{$modelKey};
        };
    }

    /**
     * @param $value
     * @param Model $model
     * @param $modelKey
     * @param $field
     * @return string
     */
    protected function serializeAttribute($value, Model $model, $modelKey, $field)
    {
        $method = 'serialize' . Str::classify($field) . 'Field';

        if (method_exists($this, $method)) {
            return $this->{$method}($value, $model);
        }

        if ($value instanceof DateTime) {
            $value = $this->serializeDateTime($value, $model);
        }

        return $value;
    }

    /**
     * @param DateTime $value
     * @param Model $model
     * @return string
     */
    protected function serializeDateTime(DateTime $value, Model $model)
    {
        return $value->format($this->getDateFormat());
    }

    /**
     * @param Model $model
     * @return bool
     */
    protected function hasCreatedAtAttribute(Model $model)
    {
        return $model->timestamps && true === $this->createdAt;
    }

    /**
     * @param Model $model
     * @return bool
     */
    protected function hasUpdatedAtAttribute(Model $model)
    {
        return $model->timestamps && true === $this->updatedAt;
    }

    /**
     * @param Model $model
     * @return bool
     */
    protected function hasDeletedAtAttribute(Model $model)
    {
        return true === $this->deletedAt && method_exists($model, 'getDeletedAtColumn');
    }

    /**
     * @return string
     */
    protected function getDateFormat()
    {
        return $this->dateFormat ?: Carbon::W3C;
    }
}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Validation;

use CloudCreativity\LaravelJsonApi\Exceptions\ValidationException;
use CloudCreativity\LaravelJsonApi\LaravelJsonApi;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Document\Error;
use Neomerx\JsonApi\Exceptions\ErrorCollection;
use Neomerx\JsonApi\Exceptions\JsonApiException;

/**
 * Class ErrorTranslator
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class ErrorTranslator
{

    /**
     * @var Translator
     */
    protected $translator;

    /**
     * Is failed meta included in generated error objects?
     *
     * @var bool
     */
    private $includeFailed;

    /**
     * ErrorTranslator constructor.
     *
     * @param Translator $translator
     */
    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
        $this->includeFailed = LaravelJsonApi::$validationFailures;
    }

    /**
     * Create an error for a member that is required.
     *
     * @param string $path
     * @param string $member
     * @return ErrorInterface
     */
    public function memberRequired(string $path, string $member): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_required', 'code'),
            $this->trans('member_required', 'title'),
            $this->trans('member_required', 'detail', compact('member')),
            $this->pointer($path)
        );
    }

    /**
     * Create an error for a member that must be an object.
     *
     * @param string $path
     * @param string $member
     * @return ErrorInterface
     */
    public function memberNotObject(string $path, string $member): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_object_expected', 'code'),
            $this->trans('member_object_expected', 'title'),
            $this->trans('member_object_expected', 'detail', compact('member')),
            $this->pointer($path, $member)
        );
    }

    /**
     * Create an error for a member that must be a resource identifier.
     *
     * @param string $path
     * @param string $member
     * @return ErrorInterface
     */
    public function memberNotIdentifier(string $path, string $member): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_identifier_expected', 'code'),
            $this->trans('member_identifier_expected', 'title'),
            $this->trans('member_identifier_expected', 'detail', compact('member')),
            $this->pointer($path, $member)
        );
    }

    /**
     * Create an error for when a member has a field that is not allowed.
     *
     * @param string $path
     * @param string $member
     * @param string $field
     * @return ErrorInterface
     */
    public function memberFieldNotAllowed(string $path, string $member, string $field): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_field_not_allowed', 'code'),
            $this->trans('member_field_not_allowed', 'title'),
            $this->trans('member_field_not_allowed', 'detail', compact('member', 'field')),
            $this->pointer($path, $member)
        );
    }

    /**
     * Create an error for a member that must be a string.
     *
     * @param string $path
     * @param string $member
     * @return ErrorInterface
     */
    public function memberNotString(string $path, string $member): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_string_expected', 'code'),
            $this->trans('member_string_expected', 'title'),
            $this->trans('member_string_expected', 'detail', compact('member')),
            $this->pointer($path, $member)
        );
    }

    /**
     * Create an error for a member that cannot be an empty value.
     *
     * @param string $path
     * @param string $member
     * @return ErrorInterface
     */
    public function memberEmpty(string $path, string $member): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('member_empty', 'code'),
            $this->trans('member_empty', 'title'),
            $this->trans('member_empty', 'detail', compact('member')),
            $this->pointer($path, $member)
        );
    }

    /**
     * Create an error for when the resource type is not supported by the endpoint.
     *
     * @param string $type
     *      the resource type that is not supported.
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceTypeNotSupported(string $type, string $path = '/data'): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_CONFLICT,
            $this->trans('resource_type_not_supported', 'code'),
            $this->trans('resource_type_not_supported', 'title'),
            $this->trans('resource_type_not_supported', 'detail', compact('type')),
            $this->pointer($path, 'type')
        );
    }

    /**
     * Create an error for when a resource type is not recognised.
     *
     * @param string $type
     *      the resource type that is not recognised.
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceTypeNotRecognised(string $type, string $path = '/data'): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('resource_type_not_recognised', 'code'),
            $this->trans('resource_type_not_recognised', 'title'),
            $this->trans('resource_type_not_recognised', 'detail', compact('type')),
            $this->pointer($path, 'type')
        );
    }

    /**
     * Create an error for when the resource id is not supported by the endpoint.
     *
     * @param string $id
     *      the resource id that is not supported.
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceIdNotSupported(string $id, string $path = '/data'): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_CONFLICT,
            $this->trans('resource_id_not_supported', 'code'),
            $this->trans('resource_id_not_supported', 'title'),
            $this->trans('resource_id_not_supported', 'detail', compact('id')),
            $this->pointer($path, 'id')
        );
    }

    /**
     * Create an error for when a resource does not support client-generated ids.
     *
     * @param string $type
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceDoesNotSupportClientIds(string $type, string $path = '/data'): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_FORBIDDEN,
            $this->trans('resource_client_ids_not_supported', 'code'),
            $this->trans('resource_client_ids_not_supported', 'title'),
            $this->trans('resource_client_ids_not_supported', 'detail', compact('type')),
            $this->pointer($path, 'id')
        );
    }

    /**
     * Create an error for a resource already existing.
     *
     * @param string $type
     *      the resource type
     * @param string $id
     *      the resource id
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceExists(string $type, string $id, string $path = '/data'): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_CONFLICT,
            $this->trans('resource_exists', 'code'),
            $this->trans('resource_exists', 'title'),
            $this->trans('resource_exists', 'detail', compact('type', 'id')),
            $this->pointer($path)
        );
    }

    /**
     * Create an error for a resource identifier that does not exist.
     *
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceDoesNotExist(string $path): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_NOT_FOUND,
            $this->trans('resource_not_found', 'code'),
            $this->trans('resource_not_found', 'title'),
            $this->trans('resource_not_found', 'detail'),
            $this->pointer($path)
        );
    }

    /**
     * Create an error for when a resource field exists in both the attributes and relationships members.
     *
     * @param string $field
     * @param string $path
     * @return ErrorInterface
     */
    public function resourceFieldExistsInAttributesAndRelationships(
        string $field,
        string $path = '/data'
    ): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('resource_field_exists_in_attributes_and_relationships', 'code'),
            $this->trans('resource_field_exists_in_attributes_and_relationships', 'title'),
            $this->trans('resource_field_exists_in_attributes_and_relationships', 'detail', compact('field')),
            $this->pointer($path)
        );
    }

    /**
     * Create an error for when a resource cannot be deleted.
     *
     * @param string|null $detail
     * @return ErrorInterface
     */
    public function resourceCannotBeDeleted(string $detail = null): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_UNPROCESSABLE_ENTITY,
            $this->trans('resource_cannot_be_deleted', 'code'),
            $this->trans('resource_cannot_be_deleted', 'title'),
            $detail ?: $this->trans('resource_cannot_be_deleted', 'detail')
        );
    }

    /**
     * Create an error for an invalid resource.
     *
     * @param string $path
     * @param string|null $detail
     *      the validation message (already translated).
     * @param array $failed
     *      rule failure details
     * @return ErrorInterface
     */
    public function invalidResource(
        string $path,
        ?string $detail = null,
        array $failed = []
    ): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_UNPROCESSABLE_ENTITY,
            $this->trans('resource_invalid', 'code'),
            $this->trans('resource_invalid', 'title'),
            $detail ?: $this->trans('resource_invalid', 'detail'),
            $this->pointer($path),
            $failed ? compact('failed') : null
        );
    }

    /**
     * Create an error for an invalid query parameter.
     *
     * @param string $param
     * @param string|null $detail
     *      the validation message (already translated).
     * @param array $failed
     *      rule failure details.
     * @return ErrorInterface
     */
    public function invalidQueryParameter(string $param, ?string $detail = null, array $failed = []): ErrorInterface
    {
        return new Error(
            null,
            null,
            Response::HTTP_BAD_REQUEST,
            $this->trans('query_invalid', 'code'),
            $this->trans('query_invalid', 'title'),
            $detail ?: $this->trans('query_invalid', 'detail'),
            [Error::SOURCE_PARAMETER => $param],
            $failed ? compact('failed') : null
        );
    }

    /**
     * Create errors for a failed validator.
     *
     * @param ValidatorContract $validator
     * @param \Closure|null $closure
     *      a closure that is bound to the translator.
     * @return ErrorCollection
     */
    public function failedValidator(ValidatorContract $validator, \Closure $closure = null): ErrorCollection
    {
        $failed = $this->doesIncludeFailed() ? $validator->failed() : [];
        $errors = new ErrorCollection();

        foreach ($validator->errors()->messages() as $key => $messages) {
            $failures = $this->createValidationFailures($failed[$key] ?? []);

            foreach ($messages as $detail) {
                $failed = $failures->shift() ?: [];

                if ($closure) {
                    $errors->add($this->call($closure, $key, $detail, $failed));
                    continue;
                }

                $errors->add(new Error(
                    null,
                    null,
                    Response::HTTP_UNPROCESSABLE_ENTITY,
                    $this->trans('failed_validator', 'code'),
                    $this->trans('failed_validator', 'title'),
                    $detail ?: $this->trans('failed_validator', 'detail')
                ));
            }
        }

        return $errors;
    }

    /**
     * Create a JSON API exception for a failed validator.
     *
     * @param ValidatorContract $validator
     * @param \Closure|null $closure
     * @return JsonApiException
     */
    public function failedValidatorException(
        ValidatorContract $validator,
        \Closure $closure = null
    ): JsonApiException
    {
        return new ValidationException(
            $this->failedValidator($validator, $closure)
        );
    }

    /**
     * Create an error by calling the closure with it bound to the error translator.
     *
     * @param \Closure $closure
     * @param mixed ...$args
     * @return ErrorInterface
     */
    public function call(\Closure $closure, ...$args): ErrorInterface
    {
        return $closure->call($this, ...$args);
    }

    /**
     * Translate an error member value.
     *
     * @param string $key
     *      the key for the JSON API error object.
     * @param string $member
     *      the JSON API error object member name.
     * @param array $replace
     * @param string|null $locale
     * @return string|null
     */
    protected function trans(string $key, string $member, array $replace = [], ?string $locale = null)
    {
        return $this->translator->trans(
            "jsonapi::errors.{$key}.{$member}",
            $replace,
            $locale
        ) ?: null;
    }

    /**
     * Create a source pointer for the specified path and optional member at that path.
     *
     * @param string $path
     * @param string|null $member
     * @return array
     */
    protected function pointer(string $path, ?string $member = null): array
    {
        /** Member can be '0' which is an empty string. */
        $withoutMember = is_null($member) || '' === $member;
        $pointer = !$withoutMember ? sprintf('%s/%s', rtrim($path, '/'), $member) : $path;

        return [Error::SOURCE_POINTER => $pointer];
    }

    /**
     * @return bool
     */
    protected function doesIncludeFailed(): bool
    {
        return $this->includeFailed;
    }

    /**
     * @param array $failures
     * @return Collection
     */
    protected function createValidationFailures(array $failures): Collection
    {
        return collect($failures)->map(function ($options, $rule) {
            return $this->createValidationFailure($rule, $options);
        })->values();
    }

    /**
     * @param string $rule
     * @param array|null $options
     * @return array
     */
    protected function createValidationFailure(string $rule, ?array $options): array
    {
        $failure = ['rule' => $this->convertRuleName($rule)];

        if (!empty($options) && $this->failedRuleHasOptions($rule)) {
            $failure['options'] = $options;
        }

        return $failure;
    }

    /**
     * @param string $rule
     * @return string
     */
    protected function convertRuleName(string $rule): string
    {
        return $this->translator->trans(
            Str::dasherize(class_basename($rule))
        );
    }

    /**
     * Should options for the rule be displayed?
     *
     * @param string $rule
     * @return bool
     */
    protected function failedRuleHasOptions(string $rule): bool
    {
        return !\in_array(strtolower($rule), [
            'exists',
            'unique',
        ], true);
    }
}


<?php

/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Schema;

use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
 * Trait CreatesEloquentIdentities
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0
 */
trait CreatesEloquentIdentities
{

    /**
     * Convert a belongs to relationship without causing the database to be queried.
     *
     * This should only be used when the related model is not going to be included in the
     * JSON API response to the client. In those circumstances, it is more efficient than
     * returning the actual related model because there will be no database query to
     * obtain the full related model.
     *
     * @param Model $model
     * @param $relationshipKey
     * @return Model
     * @deprecated
     */
    protected function createBelongsToIdentity(Model $model, $relationshipKey)
    {
        $relation = $model->{$relationshipKey}();

        if (!$relation instanceof BelongsTo) {
            throw new RuntimeException(sprintf(
                'Expecting %s on %s to be a belongs-to relationship.',
                $relationshipKey,
                get_class($model)
            ));
        }

        // support Laravel 5.8
        $foreignKey = method_exists($relation, 'getForeignKeyName') ?
            $relation->getForeignKeyName() :
            $relation->getForeignKey();

        $id = $model->{$foreignKey};

        if (is_null($id)) {
            return null;
        }

        // support Laravel 5.8
        $ownerKey = method_exists($relation, 'getOwnerKeyName') ?
            $relation->getOwnerKeyName() :
            $relation->getOwnerKey();

        $related = $relation->getRelated()->replicate();
        $related->{$ownerKey} = $id;

        return $related;
    }

    /**
     * Create a model identity using the model class and a provided id.
     *
     * @param $modelClass
     * @param string|int|null $id
     * @param string|null $keyName
     *      the key to set as the id - defaults to `Model::getRouteKeyName()`
     * @return Model|null
     * @deprecated
     */
    protected function createModelIdentity(
        $modelClass,
        $id,
        $keyName = null
    ) {
        if (is_null($id)) {
            return null;
        }

        $model = new $modelClass();

        if (!$model instanceof Model) {
            throw new RuntimeException(sprintf('Expecting a model class, got %s.', $modelClass));
        }

        $model->setAttribute($keyName ?: $model->getRouteKeyName(), $id);

        return $model;
    }
}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Eloquent\Concerns;

use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

/**
 * Trait IncludesModels
 *
 * @package CloudCreativity\LaravelJsonApi
 */
trait IncludesModels
{

    /**
     * The model relationships to eager load on every query.
     *
     * @var string[]
     */
    protected $defaultWith = [];

    /**
     * Mapping of JSON API include paths to model relationship paths.
     *
     * This adapter automatically maps include paths to model eager load
     * relationships. For example, if the JSON API include path is
     * `comments.created-by`, the model relationship `comments.createdBy`
     * will be eager loaded.
     *
     * If there are any paths that do not map directly, you can define them
     * on this property. For example, if the JSON API `comments.created-by`
     * include path actually relates to `comments.user` model path, you can
     * define that mapping here:
     *
     * ```php
     * protected $includePaths = [
     *   'comments.created-by' => 'comments.user'
     * ];
     * ```
     *
     * It is also possible to map a single JSON API include path to
     * multiple model paths. For example:
     *
     * ```php
     * protected $includePaths = [
     *   'user' => ['user.city', 'user.organization']
     * ];
     * ```
     *
     * To prevent an include path from being eager loaded, set its value
     * to `null` in the map. E.g.
     *
     * ```php
     * protected $includePaths = [
     *   'comments.author' => null,
     * ];
     * ```
     *
     * @var array
     */
    protected $includePaths = [];

    /**
     * Whether Eloquent relations are camel cased.
     *
     * @var bool
     */
    protected $camelCaseRelations = true;

    /**
     * Add eager loading to the query.
     *
     * @param Builder $query
     * @param EncodingParametersInterface $parameters
     * @return void
     */
    protected function with($query, EncodingParametersInterface $parameters)
    {
        $query->with($this->getRelationshipPaths(
            (array) $parameters->getIncludePaths()
        ));
    }

    /**
     * Add eager loading to a record.
     *
     * @param Model $record
     * @param EncodingParametersInterface $parameters
     */
    protected function load($record, EncodingParametersInterface $parameters)
    {
        $relationshipPaths = $this->getRelationshipPaths($parameters->getIncludePaths());
        $record->loadMissing($relationshipPaths);
    }

    /**
     * Get the relationship paths to eager load.
     *
     * @param Collection|array $includePaths
     *      the JSON API resource paths to be included.
     * @return array
     */
    protected function getRelationshipPaths($includePaths)
    {
        return $this
            ->convertIncludePaths($includePaths)
            ->merge($this->defaultWith)
            ->unique()
            ->all();
    }

    /**
     * @param Collection|array $includePaths
     * @return Collection
     */
    protected function convertIncludePaths($includePaths)
    {
        return collect($includePaths)->map(function ($path) {
            return $this->convertIncludePath($path);
        })->flatten()->filter()->values();
    }

    /**
     * Convert a JSON API include path to a model relationship path.
     *
     * @param $path
     * @return string|null
     */
    protected function convertIncludePath($path)
    {
        if (array_key_exists($path, $this->includePaths)) {
            return $this->includePaths[$path] ?: null;
        }

        return collect(explode('.', $path))->map(function ($segment) {
            return $this->modelRelationForField($segment);
        })->implode('.');
    }

    /**
     * Convert a JSON API field name to an Eloquent model relation name.
     *
     * According to the PSR1 spec, method names on classes MUST be camel case.
     * However, there seem to be some Laravel developers who snake case
     * relationship methods on their models, so that the method name matches
     * the snake case format of attributes (column values).
     *
     * The `$camelCaseRelations` property controls the behaviour of this
     * conversion:
     *
     * - If `true`, a field name of `user-history` or `user_history` will
     * expect the Eloquent model relation method to be `userHistory`.
     * - If `false`, a field name of `user-history` or `user_history` will
     * expect the Eloquent model relation method to be `user_history`. I.e.
     * if PSR1 is not being followed, the best guess is that method names
     * are snake case.
     *
     * If the developer has different conversion logic, they should overload
     * this method and implement it themselves.
     *
     * @param string $field
     *      the JSON API field name.
     * @return string
     *      the expected relation name on the Eloquent model.
     */
    protected function modelRelationForField($field)
    {
        return $this->camelCaseRelations ? Str::camelize($field) : Str::underscore($field);
    }
}


<?php

/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Document;

use CloudCreativity\LaravelJsonApi\Contracts\Document\MutableErrorInterface;
use InvalidArgumentException;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
use Neomerx\JsonApi\Document\Link;
use Neomerx\JsonApi\Exceptions\ErrorCollection;

/**
 * Class Error
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0 use the neomerx/jsonapi error instead.
 */
class Error implements MutableErrorInterface
{

    /**
     * @var int|string|null
     */
    private $id;

    /**
     * @var array
     */
    private $links;

    /**
     * @var string|int|null
     */
    private $status;

    /**
     * @var string|null
     */
    private $code;

    /**
     * @var string|null
     */
    private $title;

    /**
     * @var string|null
     */
    private $detail;

    /**
     * @var array
     */
    private $source;

    /**
     * @var array
     */
    private $meta;

    /**
     * @param $error
     * @return Error
     */
    public static function cast($error)
    {
        if ($error instanceof self) {
            return $error;
        } elseif (!$error instanceof ErrorInterface) {
            throw new InvalidArgumentException('Expecting an error object.');
        }

        return new self(
            $error->getId(),
            $error->getLinks(),
            $error->getStatus(),
            $error->getCode(),
            $error->getTitle(),
            $error->getDetail(),
            $error->getSource(),
            $error->getMeta()
        );
    }

    /**
     * Create an error object from an array.
     *
     * @param array|object $input
     * @return Error
     */
    public static function create($input = [])
    {
        $error = new self();
        $error->exchangeArray((array) $input);

        return $error;
    }

    /**
     * Create an error collection from an array of error arrays.
     *
     * @param array $input
     * @return ErrorCollection
     */
    public static function createMany(array $input)
    {
        $errors = new ErrorCollection();

        foreach ($input as $item) {
            $errors->add(self::create($item));
        }

        return $errors;
    }

    /**
     * @param int|string|null $id
     * @param array|null $links
     * @param int|string|null $status
     * @param int|string|null $code
     * @param string|null $title
     * @param string|null $detail
     * @param array|null $source
     * @param array|null $meta
     */
    public function __construct(
        $id = null,
        array $links = null,
        $status = null,
        $code = null,
        $title = null,
        $detail = null,
        array $source = null,
        array $meta = null
    ) {
        $this->setId($id);
        $this->setLinks($links);
        $this->setStatus($status);
        $this->setCode($code);
        $this->setTitle($title);
        $this->setDetail($detail);
        $this->setSource($source);
        $this->setMeta($meta);
    }

    /**
     * @inheritdoc
     */
    public function setId($id)
    {
        if (!is_int($id) && !is_string($id) && !is_null($id)) {
            throw new InvalidArgumentException('Expecting error id to be a string, integer or null.');
        }

        $this->id = $id;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @inheritdoc
     */
    public function hasId()
    {
        return !empty($this->id);
    }

    /**
     * @inheritdoc
     */
    public function setLinks(array $links = null)
    {
        $this->links = [];

        if ($links) {
            $this->addLinks($links);
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function addLinks(array $links = null)
    {
        foreach ((array) $links as $key => $link) {
            if (is_string($link)) {
                $link = new Link($link, null, true);
            }

            if (!$link instanceof LinkInterface) {
                throw new InvalidArgumentException('Expecting links to contain link objects.');
            }

            $this->addLink($key, $link);
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function addLink($key, LinkInterface $link)
    {
        $this->links[$key] = $link;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setAboutLink(LinkInterface $link)
    {
        $this->addLink(self::LINKS_ABOUT, $link);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getLinks()
    {
        return !empty($this->links) ? $this->links : null;
    }

    /**
     * @inheritdoc
     */
    public function setStatus($status)
    {
        if (!is_int($status) && !is_string($status) && !is_null($status)) {
            throw new InvalidArgumentException('Expecting error status to be a string, integer or null.');
        }

        $this->status = $status;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getStatus()
    {
        return $this->hasStatus() ? (string) $this->status : null;
    }

    /**
     * @inheritdoc
     */
    public function hasStatus()
    {
        return !empty($this->status);
    }

    /**
     * @inheritdoc
     */
    public function setCode($code)
    {
        if (!is_string($code) && !is_int($code) && !is_null($code)) {
            throw new InvalidArgumentException('Expecting error code to be a string, integer or null.');
        }

        $this->code = $code;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getCode()
    {
        return $this->hasCode() ? $this->code : null;
    }

    /**
     * @inheritdoc
     */
    public function hasCode()
    {
        return !empty($this->code);
    }

    /**
     * @inheritdoc
     */
    public function setTitle($title)
    {
        if (!is_string($title) && !is_null($title)) {
            throw new InvalidArgumentException('Expecting error title to be a string or null.');
        }

        $this->title = $title;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getTitle()
    {
        return $this->hasTitle() ? $this->title : null;
    }

    /**
     * @inheritdoc
     */
    public function hasTitle()
    {
        return !empty($this->title);
    }

    /**
     * @inheritdoc
     */
    public function setDetail($detail)
    {
        if (!is_string($detail) && !is_null($detail)) {
            throw new InvalidArgumentException('Expecting error detail to be a string or null.');
        }

        $this->detail = $detail;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDetail()
    {
        return $this->hasDetail() ? $this->detail : null;
    }

    /**
     * @return bool
     */
    public function hasDetail()
    {
        return !empty($this->detail);
    }

    /**
     * @inheritdoc
     */
    public function setSource(array $source = null)
    {
        $this->source = (array) $source;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getSource()
    {
        return !empty($this->source) ? $this->source : null;
    }

    /**
     * @param string|null $pointer
     * @return $this
     */
    public function setSourcePointer($pointer)
    {
        if (!is_string($pointer) && !is_null($pointer)) {
            throw new InvalidArgumentException('Expecting error source pointer to be a string or null');
        }

        if (empty($pointer)) {
            unset($this->source[self::SOURCE_POINTER]);
        } else {
            $this->source[self::SOURCE_POINTER] = $pointer;
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getSourcePointer()
    {
        return $this->hasSourcePointer() ? $this->source[self::SOURCE_POINTER] : null;
    }

    /**
     * @inheritdoc
     */
    public function hasSourcePointer()
    {
        return isset($this->source[self::SOURCE_POINTER]);
    }

    /**
     * @inheritdoc
     */
    public function setSourceParameter($parameter)
    {
        if (!is_string($parameter) && !is_null($parameter)) {
            throw new InvalidArgumentException('Expecting source parameter to be a string or null');
        }

        if (empty($parameter)) {
            unset($this->source[self::SOURCE_PARAMETER]);
        } else {
            $this->source[self::SOURCE_PARAMETER] = $parameter;
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getSourceParameter()
    {
        return $this->hasSourceParameter() ? $this->source[self::SOURCE_PARAMETER] : null;
    }

    /**
     * @inheritdoc
     */
    public function hasSourceParameter()
    {
        return isset($this->source[self::SOURCE_PARAMETER]);
    }

    /**
     * @inheritdoc
     */
    public function setMeta(array $meta = null)
    {
        $this->meta = (array) $meta;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function addMeta(array $meta)
    {
        $this->meta = array_replace_recursive($this->meta, $meta);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getMeta()
    {
        return !empty($this->meta) ? $this->meta : null;
    }

    /**
     * @inheritdoc
     */
    public function merge(ErrorInterface $error)
    {
        // Id
        if ($error->getId()) {
            $this->setId($error->getId());
        }

        // Links
        if ($error->getLinks()) {
            $this->addLinks($error->getLinks());
        }

        // Status
        if ($error->getStatus()) {
            $this->setStatus($error->getStatus());
        }

        // Code
        if ($error->getCode()) {
            $this->setCode($error->getCode());
        }

        // Title
        if ($error->getTitle()) {
            $this->setTitle($error->getTitle());
        }

        // Detail
        if ($error->getDetail()) {
            $this->setDetail($error->getDetail());
        }

        // Source
        if ($error->getSource()) {
            $this->setSource($error->getSource());
        }

        // Meta
        if ($error->getMeta()) {
            $this->addMeta($error->getMeta());
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function exchangeArray(array $input)
    {
        // Id
        if (array_key_exists(self::ID, $input)) {
            $this->setId($input[self::ID]);
        }

        // Links
        if (array_key_exists(self::LINKS, $input)) {
            $this->addLinks((array) $input[self::LINKS]);
        }

        // About Link
        if (array_key_exists(self::LINKS_ABOUT, $input) && $input[self::LINKS_ABOUT] instanceof LinkInterface) {
            $this->setAboutLink($input[self::LINKS_ABOUT]);
        }

        // Status
        if (array_key_exists(self::STATUS, $input)) {
            $this->setStatus($input[self::STATUS]);
        }

        // Code
        if (array_key_exists(self::CODE, $input)) {
            $this->setCode($input[self::CODE]);
        }

        // Title
        if (array_key_exists(self::TITLE, $input)) {
            $this->setTitle($input[self::TITLE]);
        }

        // Detail
        if (array_key_exists(self::DETAIL, $input)) {
            $this->setDetail($input[self::DETAIL]);
        }

        // Source
        if (array_key_exists(self::SOURCE, $input)) {
            $this->setSource((array) $input[self::SOURCE]);
        }

        // Source Pointer
        if (array_key_exists(self::SOURCE_POINTER, $input)) {
            $this->setSourcePointer($input[self::SOURCE_POINTER]);
        }

        // Source Parameter
        if (array_key_exists(self::SOURCE_PARAMETER, $input)) {
            $this->setSourceParameter($input[self::SOURCE_PARAMETER]);
        }

        // Meta
        if (array_key_exists(self::META, $input)) {
            $this->addMeta((array) $input[self::META]);
        }

        return $this;
    }

}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Eloquent\Concerns;

use Carbon\Carbon;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

/**
 * Trait SoftDeletesModels
 *
 * @package CloudCreativity\LaravelJsonApi
 */
trait SoftDeletesModels
{

    /**
     * @param Model $record
     * @param EncodingParametersInterface $params
     * @return bool
     */
    public function delete($record, EncodingParametersInterface $params)
    {
        return (bool) $record->forceDelete();
    }

    /**
     * @param $resourceId
     * @return Builder
     */
    protected function findQuery($resourceId)
    {
        return $this->newQuery()->withTrashed()->where(
            $this->getQualifiedKeyName(),
            $resourceId
        );
    }

    /**
     * @param Model $record
     * @param Collection $attributes
     */
    protected function fillAttributes($record, Collection $attributes)
    {
        $field = $this->getSoftDeleteField($record);

        if ($attributes->has($field)) {
            $this->fillSoftDelete($record, $field, $attributes->get($field));
        }

        $record->fill(
            $this->deserializeAttributes($attributes->forget($field), $record)
        );
    }

    /**
     * Fill the soft delete value if it has been provided.
     *
     * @param Model $record
     * @param string $field
     * @param mixed $value
     */
    protected function fillSoftDelete(Model $record, $field, $value)
    {
        $value = $this->deserializeSoftDelete(
            $value,
            $field,
            $record
        );

        $record->forceFill([
            $this->getSoftDeleteKey($record) => $value,
        ]);
    }

    /**
     * Deserialize the provided value for the soft delete attribute.
     *
     * If a boolean is provided, we interpret the soft delete value as now.
     * We check for all the boolean values accepted by the Laravel boolean
     * validator.
     *
     * @param $value
     * @param $field
     * @param $record
     * @return Carbon|null
     */
    protected function deserializeSoftDelete($value, $field, $record)
    {
        if (collect([true, false, 1, 0, '1', '0'])->containsStrict($value)) {
            return $value ? Carbon::now() : null;
        }

        return $this->deserializeAttribute($value, $field, $record);
    }

    /**
     * The JSON API field name that is used for the soft delete value.
     *
     * If none is set, defaults to the dasherized version of the model's
     * deleted_at column.
     *
     * @return string|null
     */
    protected function softDeleteField()
    {
        return property_exists($this, 'softDeleteField') ? $this->softDeleteField : null;
    }

    /**
     * Get the JSON API field that is used for the soft-delete value.
     *
     * @param Model $record
     * @return string
     */
    protected function getSoftDeleteField(Model $record)
    {
        if ($field = $this->softDeleteField()) {
            return $field;
        }

        $key = $this->getSoftDeleteKey($record);

        return Str::dasherize($key);
    }

    /**
     * Get the model key that should be used for the soft-delete value.
     *
     * @param Model $record
     * @return string
     */
    protected function getSoftDeleteKey(Model $record)
    {
        return $record->getDeletedAtColumn();
    }

    /**
     * @param $record
     * @return void
     */
    protected function persist($record)
    {
        $this->saveOrRestore($record);
    }

    /**
     * @param Model $record
     */
    protected function saveOrRestore(Model $record)
    {
        if ($this->willRestore($record)) {
           $record->restore();
       } else {
            $record->save();
        }
    }

    /**
     * @param Model $record
     * @return bool
     */
    protected function willRestore(Model $record)
    {
        if (!$record->exists) {
            return false;
        }

        if (!$key = $this->getSoftDeleteKey($record)) {
            return false;
        }

        return $record->isDirty($key) && !$record->trashed();
    }

}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * 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.
 */

namespace CloudCreativity\LaravelJsonApi\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Collection;

abstract class AbstractAllowedRule implements Rule
{

    /**
     * @var bool
     */
    private $all;

    /**
     * @var Collection
     */
    private $allowed;

    /**
     * The last value that was validated.
     *
     * @var string|null
     */
    private $value;

    /**
     * Extract parameters from the value.
     *
     * @param mixed $value
     * @return Collection
     */
    abstract protected function extract($value): Collection;

    /**
     * AllowedFilterParameters constructor.
     *
     * @param array|null $allowed
     */
    public function __construct(array $allowed = null)
    {
        $this->all = is_null($allowed);
        $this->allowed = collect($allowed)->combine($allowed);
    }

    /**
     * Add allowed parameters.
     *
     * @param string ...$params
     * @return $this
     */
    public function allow(string ...$params): self
    {
        $this->all = false;

        foreach ($params as $param) {
            $this->allowed->put($param, $param);
        }

        return $this;
    }

    /**
     * Forget an allowed parameter.
     *
     * @param string ...$params
     * @return $this
     */
    public function forget(string ...$params): self
    {
        $this->allowed->forget($params);

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function passes($attribute, $value)
    {
        $this->value = $value;

        if ($this->all) {
            return true;
        }

        return $this->extract($value)->every(function ($key) {
            return $this->allowed($key);
        });
    }

    /**
     * @inheritDoc
     */
    public function message()
    {
        $name = snake_case(class_basename($this));
        $invalid = $this->invalid();

        if ($invalid->isEmpty()) {
            $key = 'default';
        } else {
            $key = (1 === $invalid->count()) ? 'singular' : 'plural';
        }

        return trans("jsonapi::validation.{$name}.{$key}", [
            'values' => $params = $invalid->implode(', '),
        ]);
    }

    /**
     * Is the parameter allowed?
     *
     * @param string $param
     * @return bool
     */
    protected function allowed(string $param): bool
    {
        return $this->allowed->has($param);
    }

    /**
     * @return Collection
     */
    protected function invalid()
    {
        return $this->extract($this->value)->reject(function ($value) {
            return $this->allowed($value);
        });
    }

}