<?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\Middleware;

use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Codec\Codec;
use CloudCreativity\LaravelJsonApi\Codec\Decoding;
use CloudCreativity\LaravelJsonApi\Codec\Encoding;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Http\ContentNegotiatorInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\DocumentRequiredException;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Request;
use Neomerx\JsonApi\Contracts\Http\Headers\AcceptHeaderInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderParametersInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;

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

    /**
     * @var Container
     */
    private $container;

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

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

    /**
     * NegotiateContent constructor.
     *
     * @param Container $container
     * @param Factory $factory
     * @param Route $route
     */
    public function __construct(Container $container, Factory $factory, Route $route)
    {
        $this->container = $container;
        $this->factory = $factory;
        $this->route = $route;
    }

    /**
     * Handle the request.
     *
     * @param Request $request
     * @param \Closure $next
     * @param string|null $default
     *      the default negotiator to use if there is not one for the resource type.
     * @return mixed
     * @throws HttpException
     */
    public function handle($request, \Closure $next, string $default = null)
    {
        $api = $this->container->make(Api::class);
        /** @var HeaderParametersInterface $headers */
        $headers = $this->container->make(HeaderParametersInterface::class);
        $contentType = $headers->getContentTypeHeader();

        $codec = $this->factory->createCodec(
            $api->getContainer(),
            $this->matchEncoding($api, $request, $headers->getAcceptHeader(), $default),
            $decoder = $contentType ? $this->matchDecoder($api, $request, $contentType, $default) : null
        );

        $this->matched($codec);

        if (!$contentType && $this->isExpectingContent($request)) {
            throw new DocumentRequiredException();
        }

        return $next($request);
    }

    /**
     * @param Api $api
     * @param Request $request
     * @param AcceptHeaderInterface $accept
     * @param string|null $defaultNegotiator
     * @return Encoding
     */
    protected function matchEncoding(
        Api $api,
        $request,
        AcceptHeaderInterface $accept,
        ?string $defaultNegotiator
    ): Encoding
    {
        $negotiator = $this
            ->negotiator($api->getContainer(), $this->responseResourceType(), $defaultNegotiator)
            ->withRequest($request)
            ->withApi($api);

        if ($this->willSeeMany($request)) {
            return $negotiator->encodingForMany($accept);
        }

        return $negotiator->encoding($accept, $this->route->getResource());
    }

    /**
     * @param Api $api
     * @param Request $request
     * @param HeaderInterface $contentType
     * @param string|null $defaultNegotiator
     * @return Decoding|null
     */
    protected function matchDecoder(
        Api $api,
        $request,
        HeaderInterface $contentType,
        ?string $defaultNegotiator
    ): ?Decoding
    {
        $negotiator = $this
            ->negotiator($api->getContainer(), $this->route->getResourceType(), $defaultNegotiator)
            ->withRequest($request)
            ->withApi($api);

        $resource = $this->route->getResource();

        if ($resource && $field = $this->route->getRelationshipName()) {
            return $negotiator->decodingForRelationship($contentType, $resource, $field);
        }

        return $negotiator->decoding($contentType, $resource);
    }

    /**
     * Get the resource type that will be in the response.
     *
     * @return string|null
     */
    protected function responseResourceType(): ?string
    {
        return $this->route->getInverseResourceType() ?: $this->route->getResourceType();
    }

    /**
     * @param ContainerInterface $container
     * @param string|null $resourceType
     * @param string|null $default
     * @return ContentNegotiatorInterface
     */
    protected function negotiator(
        ContainerInterface $container,
        ?string $resourceType,
        ?string $default
    ): ContentNegotiatorInterface
    {
        if ($resourceType && $negotiator = $container->getContentNegotiatorByResourceType($resourceType)) {
            return $negotiator;
        }

        if ($default) {
            return $container->getContentNegotiatorByName($default);
        }

        return $this->defaultNegotiator();
    }

    /**
     * Get the default content negotiator.
     *
     * @return ContentNegotiatorInterface
     */
    protected function defaultNegotiator(): ContentNegotiatorInterface
    {
        return $this->factory->createContentNegotiator();
    }

    /**
     * Apply the matched codec.
     *
     * @param Codec $codec
     */
    protected function matched(Codec $codec): void
    {
        $this->route->setCodec($codec);
    }

    /**
     * Will the response contain a specific resource?
     *
     * E.g. for a `posts` resource, this is invoked on the following URLs:
     *
     * - `POST /posts`
     * - `GET /posts/1`
     * - `PATCH /posts/1`
     * - `DELETE /posts/1`
     *
     * I.e. a response that may contain a specified resource.
     *
     * @param Request $request
     * @return bool
     */
    public function willSeeOne($request): bool
    {
        if ($this->route->isRelationship()) {
            return false;
        }

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

        return $request->isMethod('POST');
    }

    /**
     * Will the response contain zero-to-many of a resource?
     *
     * E.g. for a `posts` resource, this is invoked on the following URLs:
     *
     * - `/posts`
     * - `/comments/1/posts`
     *
     * I.e. a response that will contain zero to many of the posts resource.
     *
     * @param Request $request
     * @return bool
     */
    public function willSeeMany($request): bool
    {
        return !$this->willSeeOne($request);
    }

    /**
     * Is data expected for the supplied request?
     *
     * If the JSON API request is any of the following, a JSON API document
     * is expected to be set on the request:
     *
     * - Create resource
     * - Update resource
     * - Replace resource relationship
     * - Add to resource relationship
     * - Remove from resource relationship
     *
     * @param Request $request
     * @return bool
     */
    protected function isExpectingContent($request): bool
    {
        $methods = $this->route->isNotRelationship() ? ['POST', 'PATCH'] : ['POST', 'PATCH', 'DELETE'];

        return \in_array($request->getMethod(), $methods);
    }

}


<?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\Pagination;

use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PagingStrategyInterface;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Utils\Arr;
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Query\QueryParametersParserInterface;

/**
 * Class CursorStrategy
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class CursorStrategy implements PagingStrategyInterface
{

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

    /**
     * @var string
     */
    private $before = 'before';

    /**
     * @var string
     */
    private $after = 'after';

    /**
     * @var string
     */
    private $limit = 'limit';

    /**
     * @var string|null
     */
    private $meta = QueryParametersParserInterface::PARAM_PAGE;

    /**
     * @var bool
     */
    private $underscoredMeta = false;

    /**
     * @var bool
     */
    private $descending = true;

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

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

    /**
     * @var mixed|null
     */
    private $columns;

    /**
     * CursorStrategy constructor.
     *
     * @param Factory $factory
     */
    public function __construct(Factory $factory)
    {
        $this->factory = $factory;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withAfterKey($key)
    {
        $this->after = $key;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withBeforeKey($key)
    {
        $this->before = $key;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withLimitKey($key)
    {
        $this->limit = $key;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withMetaKey($key)
    {
        $this->meta = $key;

        return $this;
    }

    /**
     * @return $this
     */
    public function withUnderscoredMetaKeys()
    {
        $this->underscoredMeta = true;

        return $this;
    }

    /**
     * @return $this
     */
    public function withAscending()
    {
        $this->descending = false;

        return $this;
    }

    /**
     * Set the cursor column.
     *
     * @param $column
     * @return $this
     * @todo 2.0 pass qualified columns to the cursor builder.
     */
    public function withQualifiedColumn($column)
    {
        $parts = explode('.', $column);

        if (!isset($parts[1])) {
            throw new \InvalidArgumentException('Expecting a valid qualified column name.');
        }

        $this->withColumn($parts[1]);

        return $this;
    }

    /**
     * Set the cursor column.
     *
     * @param $column
     * @return $this
     * @deprecated 2.0 use `withQualifiedColumn` instead.
     */
    public function withColumn($column)
    {
        $this->column = $column;

        return $this;
    }

    /**
     * Set the column name for the resource's ID.
     *
     * @param string $keyName
     * @return $this
     * @todo 2.0 pass qualified key name to the cursor builder.
     */
    public function withQualifiedKeyName($keyName)
    {
        $parts = explode('.', $keyName);

        if (!isset($parts[1])) {
            throw new \InvalidArgumentException('Expecting a valid qualified column name.');
        }

        $this->withIdentifierColumn($parts[1]);

        return $this;
    }

    /**
     * Set the column for the before/after identifiers.
     *
     * @param string|null $column
     * @return $this
     * @deprecated 2.0 use `withQualifiedKeyName` instead.
     */
    public function withIdentifierColumn($column)
    {
        $this->identifier = $column;

        return $this;
    }

    /**
     * Set the select columns for the query.
     *
     * @param $cols
     * @return $this
     */
    public function withColumns($cols)
    {
        $this->columns = $cols;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function paginate($query, EncodingParametersInterface $parameters)
    {
        $paginator = $this->query($query)->paginate(
            $this->cursor($parameters),
            $this->columns ?: ['*']
        );

        $parameters = $this->buildParams($parameters);

        return $this->factory->createPage(
            $paginator->getItems(),
            $this->createFirstLink($paginator, $parameters),
            $this->createPrevLink($paginator, $parameters),
            $this->createNextLink($paginator, $parameters),
            null,
            $this->createMeta($paginator),
            $this->meta
        );
    }

    /**
     * Create a new cursor query.
     *
     * @param $query
     * @return CursorBuilder
     */
    protected function query($query)
    {
        return new CursorBuilder(
            $query,
            $this->column,
            $this->identifier,
            $this->descending
        );
    }

    /**
     * Extract the cursor from the provided paging parameters.
     *
     * @param EncodingParametersInterface $parameters
     * @return Cursor
     */
    protected function cursor(EncodingParametersInterface $parameters)
    {
        return Cursor::create(
            (array) $parameters->getPaginationParameters(),
            $this->before,
            $this->after,
            $this->limit
        );
    }

    /**
     * @param CursorPaginator $paginator
     * @param array $parameters
     * @return LinkInterface
     */
    protected function createFirstLink(CursorPaginator $paginator, array $parameters = [])
    {
        return $this->createLink([
            $this->limit => $paginator->getPerPage(),
        ], $parameters);
    }

    /**
     * @param CursorPaginator $paginator
     * @param array $parameters
     * @return LinkInterface|null
     */
    protected function createNextLink(CursorPaginator $paginator, array $parameters = [])
    {
        if ($paginator->hasNoMore()) {
            return null;
        }

        return $this->createLink([
            $this->after => $paginator->lastItem(),
            $this->limit => $paginator->getPerPage(),
        ], $parameters);
    }

    /**
     * @param CursorPaginator $paginator
     * @param array $parameters
     * @return LinkInterface|null
     */
    protected function createPrevLink(CursorPaginator $paginator, array $parameters = [])
    {
        if ($paginator->isEmpty()) {
            return null;
        }

        return $this->createLink([
            $this->before => $paginator->firstItem(),
            $this->limit => $paginator->getPerPage(),
        ], $parameters);
    }

    /**
     * @param array $page
     * @param array $parameters
     * @param array|object|null $meta
     * @return LinkInterface
     */
    protected function createLink(array $page, array $parameters = [], $meta = null)
    {
        $parameters[QueryParametersParserInterface::PARAM_PAGE] = $page;

        return json_api()->links()->current($meta, $parameters);
    }

    /**
     * Build parameters that are to be included with pagination links.
     *
     * @param EncodingParametersInterface $parameters
     * @return array
     */
    protected function buildParams(EncodingParametersInterface $parameters)
    {
        return array_filter([
            QueryParametersParserInterface::PARAM_FILTER =>
                $parameters->getFilteringParameters(),
        ]);
    }

    /**
     * @param CursorPaginator $paginator
     * @return array
     */
    protected function createMeta(CursorPaginator $paginator)
    {
        $meta = [
            'per-page' => $paginator->getPerPage(),
            'from' => $paginator->getFrom(),
            'to' => $paginator->getTo(),
            'has-more' => $paginator->hasMore(),
        ];

        return $this->underscoredMeta ? Arr::underscore($meta) : $meta;
    }

}


<?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\Codec;

use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use Neomerx\JsonApi\Contracts\Encoder\EncoderInterface;
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Http\Headers\MediaType;

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

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

    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @var Encoding
     */
    private $encoding;

    /**
     * @var Decoding|null
     */
    private $decoding;

    /**
     * Codec constructor.
     *
     * @param FactoryInterface $factory
     * @param ContainerInterface $container
     * @param Encoding $encoding
     * @param Decoding|null $decoding
     */
    public function __construct(
        FactoryInterface $factory,
        ContainerInterface $container,
        Encoding $encoding,
        ?Decoding $decoding
    ) {
        $this->factory = $factory;
        $this->container = $container;
        $this->encoding = $encoding;
        $this->decoding = $decoding;
    }

    /**
     * Will the codec encode JSON API content?
     *
     * @return bool
     */
    public function willEncode(): bool
    {
        return $this->encoding->hasOptions();
    }

    /**
     * Will the codec not encode JSON API content?
     *
     * @return bool
     */
    public function willNotEncode(): bool
    {
        return !$this->willEncode();
    }

    /**
     * @return EncoderInterface
     */
    public function getEncoder(): EncoderInterface
    {
        if ($this->willNotEncode()) {
            throw new \RuntimeException('Codec does not support encoding JSON API content.');
        }

        return $this->factory->createEncoder(
            $this->container,
            $this->encoding->getOptions()
        );
    }

    /**
     * @return MediaTypeInterface
     */
    public function getEncodingMediaType(): MediaTypeInterface
    {
        return $this->encoding->getMediaType();
    }

    /**
     * Does the codec encode any of the supplied media types?
     *
     * @param string ...$mediaTypes
     * @return bool
     */
    public function encodes(string ...$mediaTypes): bool
    {
        $encoding = $this->getEncodingMediaType();

        return collect($mediaTypes)->contains(function ($mediaType, $index) use ($encoding) {
            return $encoding->equalsTo(MediaType::parse($index, $mediaType));
        });
    }

    /**
     * Will the codec decode JSON API content?
     *
     * @return bool
     */
    public function canDecodeJsonApi(): bool
    {
        if (!$this->decoding) {
            return false;
        }

        return $this->decoding->isJsonApi();
    }

    /**
     * Will the codec not decode JSON API content?
     *
     * @return bool
     */
    public function cannotDecodeJsonApi(): bool
    {
        return !$this->canDecodeJsonApi();
    }

    /**
     * @return MediaTypeInterface|null
     */
    public function getDecodingMediaType(): ?MediaTypeInterface
    {
        return $this->decoding ? $this->decoding->getMediaType() : null;
    }

    /**
     * Does the codec decode any of the supplied media types?
     *
     * @param string ...$mediaTypes
     * @return bool
     */
    public function decodes(string ...$mediaTypes): bool
    {
        if (!$decoding = $this->getDecodingMediaType()) {
            return false;
        }

        return collect($mediaTypes)->contains(function ($mediaType, $index) use ($decoding) {
            return $decoding->equalsTo(MediaType::parse($index, $mediaType));
        });
    }

    /**
     * Decode a JSON API document from the request content.
     *
     * @param $request
     * @return \stdClass|null
     */
    public function document($request): ?\stdClass
    {
        if ($this->cannotDecodeJsonApi()) {
            return null;
        }

        return $this->decoding->getJsonApiDecoder()->document($request);
    }

    /**
     * Retrieve array input from the request.
     *
     * @param $request
     * @return array
     */
    public function all($request): array
    {
        return $this->decoding ? $this->decoding->getDecoder()->decode($request) : [];
    }

}


<?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\Routing;

use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;

/**
 * Class ResourceRegistrar
 *
 * @package CloudCreativity\LaravelJsonApi
 */
final class ResourceRegistrar
{

    const KEYWORD_RELATIONSHIPS = 'relationships';
    const KEYWORD_PROCESSES = 'queue-jobs';
    const PARAM_RESOURCE_TYPE = 'resource_type';
    const PARAM_RESOURCE_ID = 'record';
    const PARAM_RELATIONSHIP_NAME = 'relationship_name';
    const PARAM_RELATIONSHIP_INVERSE_TYPE = 'relationship_inverse_type';
    const PARAM_PROCESS_TYPE = 'process_type';
    const PARAM_PROCESS_ID = 'process';

    const METHODS = [
        'index' => 'get',
        'create' => 'post',
        'read' => 'get',
        'update' => 'patch',
        'delete' => 'delete',
    ];

    use RegistersResources;

    /**
     * @var \Closure|null
     */
    private $group;

    /**
     * ResourceGroup constructor.
     *
     * @param Registrar $router
     * @param string $resourceType
     * @param array $options
     * @param \Closure|null $group
     */
    public function __construct(Registrar $router, string $resourceType, array $options = [], \Closure $group = null)
    {
        $this->router = $router;
        $this->resourceType = $resourceType;
        $this->options = $options;
        $this->group = $group;
    }

    /**
     * @return void
     */
    public function register(): void
    {
        $this->router->group($this->attributes(), function () {
            /** Custom routes */
            $this->registerCustom();

            /** Async process routes */
            if ($this->hasAsync()) {
                $this->registerProcesses();
            }

            /** Primary resource routes. */
            $this->router->group([], function () {
                $this->registerResource();
            });

            /** Resource relationship routes */
            $this->registerRelationships();
        });
    }

    /**
     * @return void
     */
    private function registerResource(): void
    {
        foreach ($this->resourceActions() as $action) {
            $this->routeForResource($action);
        }
    }

    /**
     * @return void
     */
    private function registerRelationships(): void
    {
        (new RelationshipsRegistrar($this->router, $this->resourceType, $this->options))
            ->register();
    }

    /**
     * Register custom routes.
     *
     * @return void
     */
    public function registerCustom(): void
    {
        if (!$fn = $this->group) {
            return;
        }

        $this->router->group([], function () use ($fn) {
            $fn(new RouteRegistrar(
                $this->router,
                ['controller' => $this->controller()],
                [self::PARAM_RESOURCE_TYPE => $this->resourceType]
            ));
        });
    }

    /**
     * Add routes for async processes.
     *
     * @return void
     */
    private function registerProcesses(): void
    {
        $this->routeForProcess(
            'get',
            $this->baseProcessUrl(),
            $this->actionForRoute('processes')
        );

        $this->routeForProcess(
            'get',
            $this->processUrl(),
            $this->actionForRoute('process')
        );
    }

    /**
     * @return string
     */
    private function contentNegotiation(): string
    {
        $cn = $this->options['content-negotiator'] ?? null;

        return $cn ? "json-api.content:{$cn}" : 'json-api.content';
    }

    /**
     * @return array
     */
    private function attributes(): array
    {
        return [
            'middleware' => $this->middleware(),
            'as' => "{$this->resourceType}.",
            'prefix' => $this->resourceType,
        ];
    }

    /**
     * @return array
     */
    private function middleware(): array
    {
        return collect($this->contentNegotiation())
            ->merge($this->options['middleware'] ?? [])
            ->all();
    }

    /**
     * @return array
     */
    private function resourceActions(): array
    {
        return $this->diffActions(['index', 'create', 'read', 'update', 'delete'], $this->options);
    }

    /**
     * @return bool
     */
    private function hasAsync(): bool
    {
        return $this->options['async'] ?? false;
    }

    /**
     * @return string
     */
    private function baseProcessUrl(): string
    {
        return '/' . $this->processType();
    }

    /**
     * @return string
     */
    private function processUrl(): string
    {
        return $this->baseProcessUrl() . '/' . $this->processIdParameter();
    }

    /**
     * @return string
     */
    private function processIdParameter(): string
    {
        return '{' . ResourceRegistrar::PARAM_PROCESS_ID . '}';
    }

    /**
     * @return string
     */
    private function processType(): string
    {
        return $this->options['processes'] ?? ResourceRegistrar::KEYWORD_PROCESSES;
    }

    /**
     * @param string $uri
     * @return string|null
     */
    private function idConstraintForProcess(string $uri): ?string
    {
        if (!Str::contains($uri, $this->processIdParameter())) {
            return null;
        }

        return $this->options['async_id'] ?? Uuid::VALID_PATTERN;
    }

    /**
     * @param string $method
     * @param string $uri
     * @param array $action
     * @return Route
     */
    private function routeForProcess(string $method, string $uri, array $action): Route
    {
        /** @var Route $route */
        $route = $this->router->{$method}($uri, $action);
        $route->defaults(ResourceRegistrar::PARAM_RESOURCE_TYPE, $this->resourceType);
        $route->defaults(ResourceRegistrar::PARAM_PROCESS_TYPE, $this->processType());

        if ($constraint = $this->idConstraintForProcess($uri)) {
            $route->where(ResourceRegistrar::PARAM_PROCESS_ID, $constraint);
        }

        return $route;
    }

    /**
     * @param string $action
     * @return Route
     */
    private function routeForResource(string $action): Route
    {
        return $this->createRoute(
            $this->methodForAction($action),
            $this->urlForAction($action),
            $this->actionForRoute($action)
        );
    }

    /**
     * @param string $action
     * @return string
     */
    private function urlForAction(string $action): string
    {
        if (in_array($action, ['index', 'create'], true)) {
            return $this->baseUrl();
        }

        return $this->resourceUrl();
    }

    /**
     * @param string $action
     * @return string
     */
    private function methodForAction(string $action): string
    {
        return self::METHODS[$action];
    }

    /**
     * @param string $action
     * @return array
     */
    private function actionForRoute(string $action): array
    {
        return [
            'uses' => $this->controllerAction($action),
            'as' => $action,
        ];
    }

    /**
     * @param string $action
     * @return string
     */
    private function controllerAction(string $action): string
    {
        return sprintf('%s@%s', $this->controller(), $action);
    }
}


<?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\Middleware;

use Closure;
use CloudCreativity\LaravelJsonApi\Contracts\Auth\AuthorizerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;

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

    /**
     * @var ContainerInterface
     */
    private $container;

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

    /**
     * Authorize constructor.
     *
     * @param ContainerInterface $container
     * @param Route $route
     */
    public function __construct(ContainerInterface $container, Route $route)
    {
        $this->container = $container;
        $this->route = $route;
    }

    /**
     * Handle the request.
     *
     * @param Request $request
     * @param Closure $next
     * @param string $authorizer
     * @return mixed
     * @throws AuthorizationException
     * @throws AuthenticationException
     */
    public function handle($request, Closure $next, $authorizer)
    {
        $authorizer = $this->container->getAuthorizerByName($authorizer);
        $record = $this->route->getResource();

        if ($field = $this->route->getRelationshipName()) {
            $this->authorizeRelationship(
                $authorizer,
                $request,
                $record,
                $field
            );
        } else if ($record) {
            $this->authorizeResource($authorizer, $request, $record);
        } else {
            $this->authorize($authorizer, $request, $this->route->getType());
        }

        return $next($request);
    }

    /**
     * @param AuthorizerInterface $authorizer
     * @param Request $request
     * @param string $type
     * @throws AuthenticationException
     * @throws AuthorizationException
     */
    protected function authorize(AuthorizerInterface $authorizer, $request, string $type): void
    {
        if ($request->isMethod('POST')) {
            $authorizer->create($type, $request);
            return;
        }

        $authorizer->index($type, $request);
    }

    /**
     * @param AuthorizerInterface $authorizer
     * @param Request $request
     * @param $record
     * @throws AuthenticationException
     * @throws AuthorizationException
     */
    protected function authorizeResource(AuthorizerInterface $authorizer, $request, $record): void
    {
        if ($request->isMethod('PATCH')) {
            $authorizer->update($record, $request);
            return;
        }

        if ($request->isMethod('DELETE')) {
            $authorizer->delete($record, $request);
            return;
        }

        $authorizer->read($record, $request);
    }

    /**
     * Authorize a relationship request.
     *
     * @param AuthorizerInterface $authorizer
     * @param $request
     * @param $record
     * @param string $field
     * @throws AuthenticationException
     * @throws AuthorizationException
     */
    protected function authorizeRelationship(AuthorizerInterface $authorizer, $request, $record, string $field): void
    {
        if ($this->isModifyRelationship($request)) {
            $authorizer->modifyRelationship($record, $field, $request);
            return;
        }

        $authorizer->readRelationship($record, $field, $request);
    }

    /**
     * @param Request $request
     * @return bool
     */
    protected function isModifyRelationship($request): bool
    {
        return \in_array($request->getMethod(), ['POST', 'PATCH', 'DELETE']);
    }

}


<?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\Eloquent\AbstractAdapter;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

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

    /**
     * Get the relation from the model.
     *
     * @param Model $record
     * @param string $key
     * @return Relation|Builder
     */
    protected function getRelation($record, $key)
    {
        $relation = $record->{$key}();

        if (!$relation || !$this->acceptRelation($relation)) {
            throw new RuntimeException(sprintf(
                'JSON API relation %s cannot be used for an Eloquent %s relation.',
                class_basename($this),
                class_basename($relation)
            ));
        }

        return $relation;
    }

    /**
     * Is the supplied Eloquent relation acceptable for this JSON API relation?
     *
     * @param Relation $relation
     * @return bool
     */
    protected function acceptRelation($relation)
    {
        return $relation instanceof Relation;
    }

    /**
     * Does the query need to be passed to the inverse adapter?
     *
     * @param $record
     * @param EncodingParametersInterface $parameters
     * @return bool
     */
    protected function requiresInverseAdapter($record, EncodingParametersInterface $parameters)
    {
        return !empty($parameters->getFilteringParameters()) ||
            !empty($parameters->getSortParameters()) ||
            !empty($parameters->getPaginationParameters()) ||
            !empty($parameters->getIncludePaths());
    }

    /**
     * Get an Eloquent adapter for the supplied record's relationship.
     *
     * @param Relation|Builder $relation
     * @return AbstractAdapter
     */
    protected function adapterFor($relation)
    {
        $adapter = $this->getStore()->adapterFor($relation->getModel());

        if (!$adapter instanceof AbstractAdapter) {
            throw new RuntimeException('Expecting inverse resource adapter to be an Eloquent adapter.');
        }

        return $adapter;
    }
}


<?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\Validators;

use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Contracts\Factories\FactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceObjectInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\AttributesValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\QueryValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\RelationshipsValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ResourceValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorProviderInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Http\Query\ChecksQueryParameters;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Str;
use Neomerx\JsonApi\Contracts\Http\Query\QueryCheckerInterface;

/**
 * Class AbstractValidatorProvider
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0 extend `Validation\AbstractValidators` instead.
 */
abstract class AbstractValidatorProvider implements ValidatorProviderInterface
{

    use ChecksQueryParameters;

    /**
     * The resource type the validators relate to.
     *
     * @var string
     */
    protected $resourceType;

    /**
     * Custom messages for the attributes validator.
     *
     * @var array
     */
    protected $messages = [];

    /**
     * Custom attributes for the attributes validator.
     *
     * @var array
     */
    protected $customAttributes = [];

    /**
     * Validation rules for query parameters.
     *
     * @var array
     */
    protected $queryRules = [];

    /**
     * Custom messages for the query parameters validator.
     *
     * @var array
     */
    protected $queryMessages = [];

    /**
     * Custom attributes for the query parameters validator.
     *
     * @var array
     */
    protected $queryCustomAttributes = [];

    /**
     * The allowed filtering parameters.
     *
     * By default we set this to `null` to allow any filtering parameters, as we expect
     * the filtering parameters to be validated using the query parameter validator.
     *
     * @var string[]|null
     * @see ChecksQueryParameters::allowedFilteringParameters()
     */
    protected $allowedFilteringParameters = null;

    /**
     * The allowed paging parameters.
     *
     * By default we set this to `null` to allow any paging parameters, as we expect
     * the paging parameters to be validated using the query parameter validator.
     *
     * @var string[]|null
     * @see ChecksQueryParameters::allowedPagingParameters()
     */
    protected $allowedPagingParameters = null;

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

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

    /**
     * @var ValidatorFactoryInterface
     */
    private $validatorFactory;

    /**
     * Get the validation rules for the resource attributes.
     *
     * @param object|null $record
     *      the record being updated, or null if it is a create request.
     * @return array
     */
    abstract protected function attributeRules($record = null);

    /**
     * Define the validation rules for the resource relationships.
     *
     * @param RelationshipsValidatorInterface $relationships
     * @param object|null $record
     *      the record being updated, or null if it is a create request.
     * @return void
     */
    abstract protected function relationshipRules(RelationshipsValidatorInterface $relationships, $record = null);

    /**
     * AbstractValidatorProvider constructor.
     *
     * @param Api $api
     * @param FactoryInterface $factory
     */
    public function __construct(Api $api, FactoryInterface $factory)
    {
        $this->api = $api;
        $this->factory = $factory;
    }

    /**
     * @inheritdoc
     */
    public function createResource()
    {
        $validator = $this->resourceValidator();

        return $this->validatorFactory()->resourceDocument($validator);
    }

    /**
     * @inheritdoc
     */
    public function updateResource($resourceId, $record)
    {
        $validator = $this->resourceValidator($resourceId, $record);

        return $this->validatorFactory()->resourceDocument($validator);
    }

    /**
     * @inheritdoc
     */
    public function modifyRelationship($resourceId, $relationshipName, $record)
    {
        $validator = $this
            ->resourceRelationships($record)
            ->get($relationshipName);

        return $this->validatorFactory()->relationshipDocument($validator);
    }

    /**
     * @return QueryCheckerInterface
     * @deprecated use `searchQueryChecker` instead.
     */
    public function queryChecker()
    {
        return $this->createQueryChecker($this->factory, $this->queryValidator());
    }

    /**
     * @inheritDoc
     */
    public function resourceQueryChecker()
    {
        /**
         * Allow filter params, but not sort and page, for a resource GET.
         *
         * @see https://github.com/cloudcreativity/laravel-json-api/issues/218
         */
        if (request()->isMethod('GET')) {
            return $this->factory->createExtendedQueryChecker(
                $this->allowUnrecognizedParameters(),
                $this->allowedIncludePaths(),
                $this->allowedFieldSetTypes(),
                [],
                [],
                $this->allowedFilteringParametersWithoutId(),
                $this->queryValidatorWithoutSortAndPage()
            );
        }

        /**
         * For modify resource requests, do not allow filter, sort and page.
         */
        return $this->factory->createExtendedQueryChecker(
            $this->allowUnrecognizedParameters(),
            $this->allowedIncludePaths(),
            $this->allowedFieldSetTypes(),
            [],
            [],
            [],
            $this->queryValidatorWithoutSearch()
        );
    }

    /**
     * @inheritDoc
     */
    public function searchQueryChecker()
    {
        return $this->queryChecker();
    }

    /**
     * @inheritDoc
     */
    public function relatedQueryChecker()
    {
        return $this->queryChecker();
    }

    /**
     * @inheritDoc
     */
    public function relationshipQueryChecker()
    {
        return $this->queryChecker();
    }

    /**
     * @return string
     */
    protected function getResourceType()
    {
        if (!$this->resourceType) {
            throw new RuntimeException('Expecting a resource type to be set.');
        }

        return $this->resourceType;
    }

    /**
     * Callback to configure an attributes validator.
     *
     * Child classes can override this method if they need to do custom configuration
     * on the attributes validator.
     *
     * @param Validator $validator
     *      the Laravel validator instance that will validate the attributes.
     * @param object|null $record
     *      the record being updated, or null if it is a create request.
     */
    protected function conditionalAttributes(Validator $validator, $record = null)
    {
        // no-op
    }

    /**
     * Extract attributes for validation from the supplied resource.
     *
     * Child classes can override this method if they need to customise the extraction
     * of attributes from the supplied resource. Returning null from this function means
     * the validator will extract the attributes itself. If you are customising the
     * extraction of attributes, you MUST return an array from this method.
     *
     * @param ResourceObjectInterface $resource
     * @param object|null $record
     * @return array|null
     */
    protected function extractAttributes(ResourceObjectInterface $resource, $record = null)
    {
        return null;
    }

    /**
     * @param string|null $resourceId
     * @param object|null $record
     * @return ResourceValidatorInterface
     */
    protected function resourceValidator($resourceId = null, $record = null)
    {
        return $this->validatorFactory()->resource(
            $this->getResourceType(),
            $resourceId,
            $this->resourceAttributes($record),
            $this->resourceRelationships($record),
            $this->resourceContext($record)
        );
    }

    /**
     * Get a validator for the resource attributes member.
     *
     * @param object|null $record
     * @return AttributesValidatorInterface
     */
    protected function resourceAttributes($record = null)
    {
        return $this->validatorFactory()->attributes(
            $this->attributeRules($record),
            $this->attributeMessages($record),
            $this->attributeCustomAttributes($record),
            function (Validator $validator) use ($record) {
                return $this->conditionalAttributes($validator, $record);
            },
            function (ResourceObjectInterface $resource, $record) {
                return $this->extractAttributes($resource, $record);
            }
        );
    }

    /**
     * @param object|null $record
     * @return array
     */
    protected function attributeMessages($record = null)
    {
        return $this->messages;
    }

    /**
     * @param object|null $record
     * @return array
     */
    protected function attributeCustomAttributes($record = null)
    {
        return $this->customAttributes;
    }

    /**
     * Get a validator for the resource relationships member.
     *.
     *
     * @param object|null $record
     *      the record being updated, or null if it is a create request.
     * @return RelationshipsValidatorInterface
     */
    protected function resourceRelationships($record = null)
    {
        $validator = $this->validatorFactory()->relationships();
        $this->relationshipRules($validator, $record);

        return $validator;
    }

    /**
     * Get a context validator for the resource.
     *
     * The context validator validates the whole resource, once all its constituent
     * parts have passed validation - i.e. the type, id, attributes and relationships
     * will all be valid.
     *
     * Child classes can override this method to return their own validator if
     * needed.
     *
     * @param object|null $record
     * @return ResourceValidatorInterface|null
     */
    protected function resourceContext($record = null)
    {
        return null;
    }

    /**
     * Get a validator for all query parameters.
     *
     * @return QueryValidatorInterface
     */
    protected function queryValidator()
    {
        return $this->validatorFactory()->queryParameters(
            $this->queryRules(),
            $this->queryMessages(),
            $this->queryCustomAttributes(),
            function (Validator $validator) {
                return $this->conditionalQuery($validator);
            }
        );
    }

    /**
     * Get a validator for query parameters except for filter, sort and page.
     *
     * @return QueryValidatorInterface
     */
    protected function queryValidatorWithoutSearch()
    {
        return $this->validatorFactory()->queryParameters(
            $this->queryRulesWithoutSearch(),
            $this->queryMessages(),
            $this->queryCustomAttributes(),
            function (Validator $validator) {
                return $this->conditionalQuery($validator);
            }
        );
    }

    /**
     * @return QueryValidatorInterface
     */
    protected function queryValidatorWithoutSortAndPage()
    {
        return $this->validatorFactory()->queryParameters(
            $this->queryRulesWithoutSortAndPage(),
            $this->queryMessages(),
            $this->queryCustomAttributes(),
            function (Validator $validator) {
                return $this->conditionalQuery($validator);
            }
        );
    }

    /**
     * Get the validation rules for the query parameters.
     *
     * @return array
     */
    protected function queryRules()
    {
        return $this->queryRules;
    }

    /**
     * Get the validation rules for query parameters, excluding filter, sort and page.
     *
     * @return array
     */
    protected function queryRulesWithoutSearch()
    {
        return collect($this->queryRules())->reject(function ($value, $key) {
            return Str::startsWith($key, ['filter.', 'sort.', 'page.']);
        })->all();
    }

    /**
     * Get the validation rules for query parameters excluding sort and page.
     *
     * @return array
     */
    protected function queryRulesWithoutSortAndPage()
    {
        return collect($this->queryRules())->reject(function ($value, $key) {
            return Str::startsWith($key, ['sort.', 'page.']);
        })->all();
    }

    /**
     * @return array
     */
    protected function allowedFilteringParametersWithoutId()
    {
        return collect($this->allowedFilteringParameters())->reject('id')->values()->all();
    }

    /**
     * @return array
     */
    protected function queryMessages()
    {
        return $this->queryMessages;
    }

    /**
     * @return array
     */
    protected function queryCustomAttributes()
    {
        return $this->queryCustomAttributes;
    }

    /**
     * Callback to configure a query parameter validator.
     *
     * Child classes can override this method if they need to do custom
     * configuration on the query parameter validator.
     *
     * @param Validator $validator
     *      the Laravel validator instance that will validate the query parameters.
     */
    protected function conditionalQuery(Validator $validator)
    {
        // no-op
    }

    /**
     * @return ValidatorFactoryInterface
     */
    protected function validatorFactory()
    {
        if (!$this->validatorFactory) {
            $this->validatorFactory = $this->api->validators();
        }

        return $this->validatorFactory;
    }
}


<?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\Pagination;

use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PagingStrategyInterface;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Query\QueryParametersParserInterface;

/**
 * Class StandardStrategy
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class StandardStrategy implements PagingStrategyInterface
{

    use CreatesPages;

    /**
     * @var string|null
     */
    protected $pageKey;

    /**
     * @var string|null
     */
    protected $perPageKey;

    /**
     * @var array|null
     */
    protected $columns;

    /**
     * @var bool|null
     */
    protected $simplePagination;

    /**
     * @var bool|null
     */
    protected $underscoreMeta;

    /**
     * @var string|null
     */
    protected $metaKey;

    /**
     * @var string|null
     */
    protected $primaryKey;

    /**
     * StandardStrategy constructor.
     */
    public function __construct()
    {
        $this->metaKey = QueryParametersParserInterface::PARAM_PAGE;
    }

    /**
     * Set the qualified column name that is being used for the resource's ID.
     *
     * @param $keyName
     * @return $this
     */
    public function withQualifiedKeyName($keyName)
    {
        $this->primaryKey = $keyName;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withPageKey($key)
    {
        $this->pageKey = $key;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function withPerPageKey($key)
    {
        $this->perPageKey = $key;

        return $this;
    }

    /**
     * @param $cols
     * @return $this;
     */
    public function withColumns($cols)
    {
        $this->columns = $cols;

        return $this;
    }

    /**
     * @return $this
     */
    public function withSimplePagination()
    {
        $this->simplePagination = true;

        return $this;
    }

    /**
     * @return $this
     */
    public function withUnderscoredMetaKeys()
    {
        $this->underscoreMeta = true;

        return $this;
    }

    /**
     * Set the key for the paging meta.
     *
     * Use this to 'nest' the paging meta in a sub-key of the JSON API document's top-level meta object.
     * A string sets the key to use for nesting. Use `null` to indicate no nesting.
     *
     * @param string|null $key
     * @return $this
     */
    public function withMetaKey($key)
    {
        $this->metaKey = $key ?: null;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function paginate($query, EncodingParametersInterface $parameters)
    {
        $pageParameters = collect((array) $parameters->getPaginationParameters());

        $paginator = $this
            ->defaultOrder($query)
            ->query($query, $pageParameters);

        return $this->createPage($paginator, $parameters);
    }

    /**
     * @param Collection $collection
     * @return int
     */
    protected function getPerPage(Collection $collection)
    {
        return (int) $collection->get($this->getPerPageKey());
    }

    /**
     * Get the default per-page value for the query.
     *
     * If the query is an Eloquent builder, we can pass in `null` as the default,
     * which then delegates to the model to get the default. Otherwise the Laravel
     * standard default is 15.
     *
     * @param $query
     * @return int|null
     */
    protected function getDefaultPerPage($query)
    {
        return $query instanceof EloquentBuilder ? null : 15;
    }

    /**
     * @return array
     */
    protected function getColumns()
    {
        return $this->columns ?: ['*'];
    }

    /**
     * @return bool
     */
    protected function isSimplePagination()
    {
        return (bool) $this->simplePagination;
    }

    /**
     * @param $query
     * @return bool
     */
    protected function willSimplePaginate($query)
    {
        return $this->isSimplePagination() && method_exists($query, 'simplePaginate');
    }

    /**
     * Apply a deterministic order to the page.
     *
     * @param Builder|Relation $query
     * @return $this
     * @see https://github.com/cloudcreativity/laravel-json-api/issues/313
     */
    protected function defaultOrder($query)
    {
        if ($this->doesRequireOrdering($query)) {
            $query->orderBy($this->primaryKey);
        }

        return $this;
    }

    /**
     * Do we need to apply a deterministic order to the query?
     *
     * If the primary key has not been used for a sort order already, we use it
     * to ensure the page has a deterministic order.
     *
     * @param Builder|Relation $query
     * @return bool
     */
    protected function doesRequireOrdering($query)
    {
        if (!$this->primaryKey) {
            return false;
        }

        $query = ($query instanceof Relation) ? $query->getBaseQuery() : $query->getQuery();

        return !collect($query->orders ?: [])->contains(function (array $order) {
            $col = $order['column'] ?? '';
            return $this->primaryKey === $col;
        });
    }

    /**
     * @param Builder|Relation $query
     * @param Collection $pagingParameters
     * @return mixed
     */
    protected function query($query, Collection $pagingParameters)
    {
        $pageName = $this->getPageKey();
        $size = $this->getPerPage($pagingParameters) ?: $this->getDefaultPerPage($query);
        $cols = $this->getColumns();

        return $this->willSimplePaginate($query) ?
            $query->simplePaginate($size, $cols, $pageName) :
            $query->paginate($size, $cols, $pageName);
    }

}


<?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\Routing;

use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Ramsey\Uuid\Uuid;

/**
 * Class ResourceRegistration
 *
 * @package CloudCreativity\LaravelJsonApi
 */
final class ResourceRegistration implements Arrayable
{

    /**
     * @var Registrar
     */
    private $router;

    /**
     * @var string
     */
    private $resourceType;

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

    /**
     * @var RelationshipsRegistration
     */
    private $relationships;

    /**
     * Custom routes.
     *
     * @var \Closure|null
     */
    private $routes;

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

    /**
     * ResourceRegistration constructor.
     *
     * @param Registrar $router
     * @param string $resourceType
     * @param array $options
     */
    public function __construct(Registrar $router, string $resourceType, array $options = [])
    {
        $this->router = $router;
        $this->resourceType = $resourceType;
        $this->registered = false;

        $this->options = collect($options)
            ->forget(['has-one', 'has-many'])
            ->all();

        if (isset($options['controller']) && true === $options['controller']) {
            $this->controller();
        }

        $this->relationships = new RelationshipsRegistration(
            $options['has-one'] ?? null,
            $options['has-many'] ?? null
        );
    }

    /**
     * Set an authorizer for the resource.
     *
     * @param string $authorizer
     * @return $this
     */
    public function authorizer(string $authorizer): self
    {
        return $this->middleware("json-api.auth:{$authorizer}");
    }

    /**
     * Add middleware.
     *
     * @param string ...$middleware
     * @return $this
     */
    public function middleware(string ...$middleware): self
    {
        $this->options['middleware'] = array_merge(
            Arr::wrap($this->options['middleware'] ?? []),
            $middleware
        );

        return $this;
    }

    /**
     * @param string $controller
     * @return $this
     */
    public function controller(string $controller = ''): self
    {
        $this->options['controller'] = $controller ?: Str::classify($this->resourceType) . 'Controller';

        return $this;
    }

    /**
     * @param string $negotiator
     * @return $this
     */
    public function contentNegotiator(string $negotiator): self
    {
        $this->options['content-negotiator'] = $negotiator;

        return $this;
    }

    /**
     * @param string|null $constraint
     * @return $this
     */
    public function id(?string $constraint): self
    {
        $this->options['id'] = $constraint;

        return $this;
    }

    /**
     * @param string ...$actions
     * @return $this
     */
    public function only(string ...$actions): self
    {
        $this->options['only'] = $actions;

        return $this;
    }

    /**
     * @param string ...$actions
     * @return $this
     */
    public function except(string ...$actions): self
    {
        $this->options['except'] = $actions;

        return $this;
    }

    /**
     * @return $this
     */
    public function readOnly(): self
    {
        return $this->only('index', 'read');
    }

    /**
     * @param \Closure $closure
     * @return $this
     */
    public function relationships(\Closure $closure): self
    {
        $closure($this->relationships);

        return $this;
    }

    /**
     * @param string $constraint
     * @return $this
     */
    public function async(string $constraint = Uuid::VALID_PATTERN): ResourceRegistration
    {
        $this->options['async'] = true;
        $this->options['async_id'] = $constraint;

        return $this;
    }

    /**
     * @param \Closure $routes
     * @return $this
     */
    public function routes(\Closure $routes): self
    {
        $this->routes = $routes;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function toArray()
    {
        return collect($this->options)->merge($this->relationships)->all();
    }

    /**
     * @return void
     */
    public function register(): void
    {
        $this->registered = true;

        $group = new ResourceRegistrar($this->router, $this->resourceType, $this->toArray(), $this->routes);
        $group->register($this->router);
    }

    /**
     * @return void
     */
    public function __destruct()
    {
        if (!$this->registered) {
            $this->register();
        }
    }

}


<?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\Support\Collection;

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

    /**
     * Mapping of JSON API attribute field names to model keys.
     *
     * By default, JSON API attribute fields will automatically be converted to the
     * underscored or camel cased equivalent for the model key. E.g. if the model
     * uses snake case, the JSON API field `published-at` will be converted
     * to `published_at`. If the model does not use snake case, it will be converted
     * to `publishedAt`.
     *
     * For any fields that do not directly convert to model keys, you can list them
     * here. For example, if the JSON API field `published-at` needed to map to the
     * `published_date` model key, then it can be listed as follows:
     *
     * ```php
     * protected $attributes = [
     *   'published-at' => 'published_date',
     * ];
     * ```
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * The resource attributes that are dates.
     *
     * A list of JSON API attribute fields that should be cast to dates. If this is
     * empty, then `Model::getDates()` will be used.
     *
     * @var string[]
     */
    protected $dates = [];

    /**
     * Fill JSON API attributes into a model.
     *
     * @param Model $record
     * @param $attributes
     */
    protected function fillAttributes($record, Collection $attributes)
    {
        $record->fill(
            $this->deserializeAttributes($attributes, $record)
        );
    }

    /**
     * Convert a JSON API resource field name to a model key.
     *
     * @param $field
     * @param Model $model
     * @return string
     */
    protected function modelKeyForField($field, $model)
    {
        if (isset($this->attributes[$field])) {
            return $this->attributes[$field];
        }

        $key = $model::$snakeAttributes ? Str::underscore($field) : Str::camelize($field);

        return $this->attributes[$field] = $key;
    }

    /**
     * Deserialize fillable attributes.
     *
     * @param $attributes
     * @param $record
     * @return array
     */
    protected function deserializeAttributes($attributes, $record)
    {
        return collect($attributes)->reject(function ($v, $field) use ($record) {
            return $this->isNotFillable($field, $record);
        })->mapWithKeys(function ($value, $field) use ($record) {
            $key = $this->modelKeyForField($field, $record);

            return [$key => $this->deserializeAttribute($value, $field, $record)];
        })->all();
    }

    /**
     * Deserialize a value obtained from the resource's attributes.
     *
     * @param $value
     *      the value that the client provided.
     * @param $field
     *      the attribute key for the value
     * @param Model $record
     * @return Carbon|null
     */
    protected function deserializeAttribute($value, $field, $record)
    {
        if ($this->isDateAttribute($field, $record)) {
            return $this->deserializeDate($value, $field, $record);
        }

        $method = 'deserialize' . Str::classify($field) . 'Field';

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

        return $value;
    }

    /**
     * Convert a JSON date into a PHP date time object.
     *
     * @param $value
     *      the value in the JSON API resource attribute field.
     * @param string $field
     *      the JSON API field name being deserialized.
     * @param Model $record
     *      the domain record being filled.
     * @return Carbon|null
     */
    protected function deserializeDate($value, $field, $record)
    {
        return !is_null($value) ? new Carbon($value) : null;
    }

    /**
     * Is this resource key a date attribute?
     *
     * @param $field
     * @param Model $record
     * @return bool
     */
    protected function isDateAttribute($field, $record)
    {
        if (empty($this->dates)) {
            return in_array($this->modelKeyForField($field, $record), $record->getDates(), true);
        }

        return in_array($field, $this->dates, 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\Queue;

use Carbon\Carbon;
use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Object\ResourceIdentifier;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;

class ClientJob extends Model implements AsynchronousProcess
{

    /**
     * @var bool
     */
    public $incrementing = false;

    /**
     * @var string
     */
    protected $table = 'json_api_client_jobs';

    /**
     * @var string
     */
    protected $primaryKey = 'uuid';

    /**
     * Mass-assignable attributes.
     *
     * @var array
     */
    protected $fillable = [
        'api',
        'attempts',
        'completed_at',
        'failed',
        'resource_type',
        'resource_id',
        'timeout',
        'timeout_at',
        'tries',
    ];

    /**
     * Default attributes.
     *
     * @var array
     */
    protected $attributes = [
        'failed' => false,
        'attempts' => 0,
    ];

    /**
     * @var array
     */
    protected $casts = [
        'attempts' => 'integer',
        'failed' => 'boolean',
        'timeout' => 'integer',
        'tries' => 'integer',
    ];

    /**
     * @var array
     */
    protected $dates = [
        'completed_at',
        'timeout_at',
    ];

    /**
     * @inheritdoc
     */
    public static function boot()
    {
        parent::boot();

        static::addGlobalScope(new ClientJobScope());

        static::creating(function (ClientJob $job) {
            $job->uuid = $job->uuid ?: Uuid::uuid4()->toString();
        });
    }

    /**
     * @inheritDoc
     */
    public function getResourceType(): string
    {
        if (!$type = $this->resource_type) {
            throw new RuntimeException('No resource type set.');
        }

        return $type;
    }

    /**
     * @inheritDoc
     */
    public function getLocation(): ?string
    {
        if ($this->failed) {
            return null;
        }

        $type = $this->resource_type;
        $id = $this->resource_id;

        if (!$type || !$id) {
            return null;
        }

        return $this->getApi()->url()->read($type, $id);
    }

    /**
     * @inheritDoc
     */
    public function isPending(): bool
    {
        return !$this->offsetExists('completed_at');
    }

    /**
     * @inheritDoc
     */
    public function dispatching(ClientDispatch $dispatch): void
    {
        $this->fill([
            'api' => $dispatch->getApi(),
            'resource_type' => $dispatch->getResourceType(),
            'resource_id' => $dispatch->getResourceId(),
            'timeout' => $dispatch->getTimeout(),
            'timeout_at' => $dispatch->getTimeoutAt(),
            'tries' => $dispatch->getMaxTries(),
        ])->save();
    }

    /**
     * @inheritDoc
     */
    public function processed($job): void
    {
        $this->update([
            'attempts' => $job->attempts(),
            'completed_at' => $job->isDeleted() ? Carbon::now() : null,
            'failed' => $job->hasFailed(),
        ]);
    }

    /**
     * @return Api
     */
    public function getApi(): Api
    {
        if (!$api = $this->api) {
            throw new RuntimeException('Expecting API to be set on client job.');
        }

        return json_api($api);
    }

    /**
     * Set the resource that the client job relates to.
     *
     * @param mixed $resource
     * @return ClientJob
     */
    public function setResource($resource): ClientJob
    {
        $schema = $this->getApi()->getContainer()->getSchema($resource);

        $this->fill([
            'resource_type' => $schema->getResourceType(),
            'resource_id' => $schema->getId($resource),
        ]);

        return $this;
    }

    /**
     * Get the resource that the process relates to.
     *
     * @return mixed|null
     */
    public function getResource()
    {
        if (!$this->resource_type || !$this->resource_id) {
            return null;
        }

        return $this->getApi()->getStore()->find(
            ResourceIdentifier::create($this->resource_type, (string) $this->resource_id)
        );
    }

}


<?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\Routing;

use CloudCreativity\LaravelJsonApi\Codec\Codec;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceIdentifierInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Contracts\Resolver\ResolverInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\ResourceNotFoundException;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Object\ResourceIdentifier;
use Illuminate\Routing\Route as IlluminateRoute;

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

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

    /**
     * @var ResolverInterface
     */
    private $resolver;

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

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

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

    /**
     * Route constructor.
     *
     * @param ResolverInterface $resolver
     * @param IlluminateRoute|null $route
     *      the route, if one was successfully matched.
     */
    public function __construct(ResolverInterface $resolver, ?IlluminateRoute $route)
    {
        $this->resolver = $resolver;
        $this->route = $route;
    }

    /**
     * Substitute the route bindings onto the Laravel route.
     *
     * @param StoreInterface $store
     * @return void
     * @throws ResourceNotFoundException
     */
    public function substituteBindings(StoreInterface $store): void
    {
        /** Cache the ID values so that we still have access to them. */
        $this->resourceId = $this->getResourceId() ?: false;
        $this->processId = $this->getProcessId() ?: false;

        /** Bind the domain record. */
        if ($this->resourceId) {
            $this->route->setParameter(
                ResourceRegistrar::PARAM_RESOURCE_ID,
                $store->findOrFail($this->getResourceType(), $this->resourceId)
            );
        }

        /** Bind the async process. */
        if ($this->processId) {
            $this->route->setParameter(
                ResourceRegistrar::PARAM_PROCESS_ID,
                $store->findOrFail($this->getProcessType(), $this->processId)
            );
        }
    }

    /**
     * Set the matched codec.
     *
     * @param Codec $codec
     * @return $this
     */
    public function setCodec(Codec $codec): self
    {
        $this->codec = $codec;

        return $this;
    }

    /**
     * Get the matched codec.
     *
     * @return Codec
     */
    public function getCodec(): Codec
    {
        if (!$this->hasCodec()) {
            throw new RuntimeException('Codec cannot be obtained before content negotiation.');
        }

        return $this->codec;
    }

    /**
     * @return bool
     */
    public function hasCodec(): bool
    {
        return !!$this->codec;
    }

    /**
     * Get the domain record type for the route.
     *
     * @return string
     */
    public function getType(): string
    {
        if ($resource = $this->getResource()) {
            return get_class($resource);
        }

        $resourceType = $this->getResourceType();

        if (!$type = $this->resolver->getType($resourceType)) {
            throw new RuntimeException("JSON API resource type {$resourceType} is not registered.");
        }

        return $type;
    }

    /**
     * What is the resource type of the route?
     *
     * @return string|null
     *      the resource type
     */
    public function getResourceType(): ?string
    {
        return $this->parameter(ResourceRegistrar::PARAM_RESOURCE_TYPE);
    }

    /**
     * What is the resource id of the route?
     *
     * @return string|null
     */
    public function getResourceId(): ?string
    {
        if (is_null($this->resourceId)) {
            return $this->parameter(ResourceRegistrar::PARAM_RESOURCE_ID);
        }

        return $this->resourceId ?: null;
    }

    /**
     * Get the resource identifier for the route.
     *
     * @return ResourceIdentifierInterface|null
     * @deprecated 2.0.0
     */
    public function getResourceIdentifier(): ?ResourceIdentifierInterface
    {
        if (!$resourceId = $this->getResourceId()) {
            return null;
        }

        return ResourceIdentifier::create($this->getResourceType(), $resourceId);
    }

    /**
     * Get the domain object binding for the route.
     *
     * @return mixed|null
     */
    public function getResource()
    {
        $resource = $this->parameter(ResourceRegistrar::PARAM_RESOURCE_ID);

        return is_object($resource) ? $resource : null;
    }

    /**
     * Get the relationship name for the route.
     *
     * @return string|null
     */
    public function getRelationshipName(): ?string
    {
        return $this->parameter(ResourceRegistrar::PARAM_RELATIONSHIP_NAME);
    }

    /**
     * Get the the inverse resource type for the route.
     *
     * For example, a `GET /posts/1/author`, the string returned by this method
     * would be `users` if the related author is a `users` JSON API resource type.
     *
     * @return string|null
     */
    public function getInverseResourceType(): ?string
    {
        return $this->parameter(ResourceRegistrar::PARAM_RELATIONSHIP_INVERSE_TYPE);
    }

    /**
     * Get the process resource type for the route.
     *
     * @return string|null
     */
    public function getProcessType(): ?string
    {
        return $this->parameter(ResourceRegistrar::PARAM_PROCESS_TYPE);
    }

    /**
     * Get the process id for the route.
     *
     * @return string|null
     */
    public function getProcessId(): ?string
    {
        if (is_null($this->processId)) {
            return $this->parameter(ResourceRegistrar::PARAM_PROCESS_ID);
        }

        return $this->processId ?: null;
    }

    /**
     * Get the process binding for the route.
     *
     * @return AsynchronousProcess|null
     */
    public function getProcess(): ?AsynchronousProcess
    {
        $process = $this->parameter(ResourceRegistrar::PARAM_PROCESS_ID);

        return ($process instanceof AsynchronousProcess) ? $process : null;
    }

    /**
     * Get the process identifier for the route.
     *
     * @return ResourceIdentifierInterface|null
     * @deprecated 2.0.0
     */
    public function getProcessIdentifier(): ?ResourceIdentifierInterface
    {
        if (!$id = $this->getProcessId()) {
            return null;
        }

        return ResourceIdentifier::create($this->getProcessType(), $id);
    }

    /**
     * @return bool
     */
    public function isResource(): bool
    {
        return !empty($this->getResourceId());
    }

    /**
     * @return bool
     */
    public function isNotResource(): bool
    {
        return !$this->isResource();
    }

    /**
     * @return bool
     */
    public function isRelationship(): bool
    {
        return !empty($this->getRelationshipName());
    }

    /**
     * @return bool
     */
    public function isNotRelationship(): bool
    {
        return !$this->isRelationship();
    }

    /**
     * @return bool
     */
    public function isProcesses(): bool
    {
        return !empty($this->getProcessType());
    }

    /**
     * @return bool
     */
    public function isNotProcesses(): bool
    {
        return !$this->isProcesses();
    }

    /**
     * @return bool
     */
    public function isProcess(): bool
    {
        return !empty($this->getProcessId());
    }

    /**
     * @return bool
     */
    public function isNotProcess(): bool
    {
        return !$this->isProcess();
    }

    /**
     * @param string $name
     * @param mixed $default
     * @return mixed
     */
    private function parameter(string $name, $default = null)
    {
        return $this->route ? $this->route->parameter($name, $default) : null;
    }

}


<?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\Services;

use Closure;
use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Api\Repository;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Http\Requests\JsonApiRequest;
use CloudCreativity\LaravelJsonApi\LaravelJsonApi;
use CloudCreativity\LaravelJsonApi\Routing\ApiRegistration;
use CloudCreativity\LaravelJsonApi\Routing\JsonApiRegistrar;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Contracts\Container\Container;

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

    /**
     * @var Container
     */
    private $container;

    /**
     * JsonApiService constructor.
     *
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * Set or get the default API name.
     *
     * @param string|null $apiName
     * @return string
     * @deprecated 2.0.0 setting the API name via this method will be removed (getter will remain).
     */
    public function defaultApi($apiName = null)
    {
        if (is_null($apiName)) {
            return LaravelJsonApi::$defaultApi;
        }

        LaravelJsonApi::defaultApi($apiName);

        return $apiName;
    }

    /**
     * Get an API by name.
     *
     * @param string|null $apiName
     * @return Api
     * @throws RuntimeException
     *      if the API name is invalid.
     */
    public function api($apiName = null)
    {
        /** @var Repository $repo */
        $repo = $this->container->make(Repository::class);

        return $repo->createApi($apiName ?: $this->defaultApi());
    }

    /**
     * Get the current JSON API route.
     *
     * @return Route
     */
    public function currentRoute(): Route
    {
        return $this->container->make(Route::class);
    }

    /**
     * Get the JSON API request.
     *
     * @return JsonApiRequest
     * @deprecated 2.0.0 use `current()`
     */
    public function request()
    {
        return $this->container->make('json-api.request');
    }

    /**
     * Get the API that is handling the inbound HTTP request.
     *
     * @return Api|null
     *      the API, or null if the there is no inbound JSON API HTTP request.
     */
    public function requestApi()
    {
        if (!$this->container->bound('json-api.inbound')) {
            return null;
        }

        return $this->container->make('json-api.inbound');
    }

    /**
     * Get either the request API or the default API.
     *
     * @return Api
     */
    public function requestApiOrDefault()
    {
        return $this->requestApi() ?: $this->api();
    }

    /**
     * @return Api
     * @throws RuntimeException
     *      if there is no JSON API handling the inbound request.
     */
    public function requestApiOrFail()
    {
        if (!$api = $this->requestApi()) {
            throw new RuntimeException('No JSON API handling the inbound request.');
        }

        return $api;
    }

    /**
     * Register the routes for an API.
     *
     * @param $apiName
     * @param array|Closure $options
     * @param Closure|null $routes
     * @return ApiRegistration
     */
    public function register($apiName, $options = [], Closure $routes = null): ApiRegistration
    {
        /** @var JsonApiRegistrar $registrar */
        $registrar = $this->container->make('json-api.registrar');

        return $registrar->api($apiName, $options, $routes);
    }

}


<?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\Requests;

use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorProviderInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\ValidationException;
use CloudCreativity\LaravelJsonApi\Object\Document;

/**
 * Class CreateResource
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class CreateResource extends ValidatedRequest
{

    /**
     * @inheritDoc
     */
    protected function authorize()
    {
        if (!$authorizer = $this->getAuthorizer()) {
            return;
        }

        $authorizer->create($this->getType(), $this->request);
    }

    /**
     * @inheritDoc
     */
    protected function validateQuery()
    {
        if (!$validators = $this->getValidators()) {
            return;
        }

        /** Pre-1.0 validators */
        if ($validators instanceof ValidatorProviderInterface) {
            $validators->resourceQueryChecker()->checkQuery($this->getEncodingParameters());
            return;
        }

        /** 1.0 validators */
        $this->passes(
            $validators->modifyQuery($this->query())
        );
    }

    /**
     * @inheritDoc
     */
    protected function validateDocument()
    {
        $document = $this->decode();
        $validators = $this->getValidators();

        /** Pre-1.0 validators */
        if ($validators instanceof ValidatorProviderInterface) {
            $this->validateDocumentWithProvider($validators, $document);
            return;
        }

        /** If there is a decoded JSON API document, check it complies with the spec. */
        if ($document) {
            $this->validateDocumentCompliance($document, $validators);
        }

        /** Check the document is logically correct. */
        if ($validators) {
            $this->passes($validators->create($this->all()));
        }
    }

    /**
     * Validate the JSON API document complies with the spec.
     *
     * @param object $document
     * @param ValidatorFactoryInterface|null $validators
     */
    protected function validateDocumentCompliance($document, ?ValidatorFactoryInterface $validators): void
    {
        $this->passes(
            $this->factory->createNewResourceDocumentValidator(
                $document,
                $this->getResourceType(),
                $validators && $validators->supportsClientIds()
            )
        );
    }

    /**
     * @param ValidatorProviderInterface $validators
     * @param $document
     * @deprecated 2.0.0
     */
    protected function validateDocumentWithProvider(ValidatorProviderInterface $validators, $document)
    {
        $validator = $validators->createResource();

        if (!$validator->isValid(new Document($document))) {
            throw new ValidationException($validator->getErrors());
        }
    }

}