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

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

    /**
     * @var array
     */
    private static $dasherized = [];

    /**
     * @var array
     */
    private static $decamelized = [];

    /**
     * @var array
     */
    private static $underscored = [];

    /**
     * @var array
     */
    private static $camelized = [];

    /**
     * @var array
     */
    private static $classified = [];

    /**
     * Replaces underscores or camel case with dashes.
     *
     * @param string $value
     * @return string
     */
    public static function dasherize($value)
    {
        if (isset(self::$dasherized[$value])) {
            return self::$dasherized[$value];
        }

        return self::$dasherized[$value] = str_replace('_', '-', self::decamelize($value));
    }

    /**
     * Converts a camel case string into all lower case separated by underscores.
     *
     * @param string $value
     * @return string
     */
    public static function decamelize($value)
    {
        if (isset(self::$decamelized[$value])) {
            return self::$decamelized[$value];
        }

        return self::$decamelized[$value] = strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1_', $value));
    }

    /**
     * Converts a camel case or dasherized string into a lower cased and underscored string.
     *
     * @param $value
     * @return string
     */
    public static function underscore($value)
    {
        if (isset(self::$underscored[$value])) {
            return self::$underscored[$value];
        }

        return self::$underscored[$value] = str_replace('-', '_', self::decamelize($value));
    }

    /**
     * Gets the lower camel case form of a string.
     *
     * @param string $value
     * @return string
     */
    public static function camelize($value)
    {
        if (isset(self::$camelized[$value])) {
            return self::$camelized[$value];
        }

        return self::$camelized[$value] = lcfirst(self::classify($value));
    }

    /**
     * Gets the upper camel case form of a string.
     *
     * @param string $value
     * @return string
     */
    public static function classify($value)
    {
        if (isset(self::$classified[$value])) {
            return self::$classified[$value];
        }

        $converted = ucwords(str_replace(['-', '_'], ' ', $value));

        return self::$classified[$value] = str_replace(' ', '', $converted);
    }
}


<?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\Console\Commands;

use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Api\Repository;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

/**
 * Class AbstractGeneratorCommand
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractGeneratorCommand extends GeneratorCommand
{

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name;

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description;

    /**
     * The type of class being generated.
     *
     * @var string
     */
    protected $type;

    /**
     * Whether the resource type is non-dependent on eloquent.
     *
     * @var boolean
     */
    protected $isIndependent = false;

    /**
     * The location of all generator stubs
     *
     * @var string
     */
    private $stubsDirectory;

    /**
     * @var Repository
     */
    private $apiRepository;

    /**
     * AbstractGeneratorCommand constructor.
     *
     * @param Filesystem $files
     * @param Repository $apiRepository
     */
    public function __construct(Filesystem $files, Repository $apiRepository)
    {
        parent::__construct($files);
        $this->apiRepository = $apiRepository;
        $this->stubsDirectory = __DIR__ . '/../../../stubs';
    }

    /**
     * @return int
     */
    public function handle()
    {
        if (!$this->apiRepository->exists($api = $this->getApiName())) {
            $this->error("JSON API '$api' does not exist.");
            return 1;
        }

        return (parent::handle() !== false) ? 0 : 1;
    }

    /**
     * Get the desired class name from the input.
     *
     * @return string
     */
    protected function getNameInput()
    {
        if (!$this->isByResource()) {
            return $this->getResourceName();
        }

        return $this->type;
    }

    /**
     * @param string $name
     * @return string
     */
    protected function qualifyClass($name)
    {
        $resolver = $this->getApi()->getDefaultResolver();
        $method = "get{$this->type}ByResourceType";

        return $resolver->{$method}($this->getResourceName());
    }

    /**
     * Build the class with the given name.
     *
     * @param  string $name
     * @return string
     */
    protected function buildClass($name)
    {
        $stub = $this->files->get($this->getStub());

        $this->replaceNamespace($stub, $name)
            ->replaceClassName($stub, $name)
            ->replaceResourceType($stub)
            ->replaceApplicationNamespace($stub)
            ->replaceRecord($stub);

        return $stub;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [
            ['resource', InputArgument::REQUIRED, "The resource for which a {$this->type} class will be generated."],
            ['api', InputArgument::OPTIONAL, "The API that the resource belongs to."],
        ];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        if ($this->isIndependent) {
            return [];
        }

        return [
            ['eloquent', 'e', InputOption::VALUE_NONE, 'Use Eloquent classes.'],
            ['no-eloquent', 'N', InputOption::VALUE_NONE, 'Do not use Eloquent classes.'],
        ];
    }

    /**
     * Get the stub file for the generator.
     *
     * @return string
     */
    protected function getStub()
    {
        if ($this->isIndependent) {
            return $this->getStubFor('independent');
        }

        if ($this->isEloquent()) {
            return $this->getStubFor('eloquent');
        }

        return $this->getStubFor('abstract');
    }

    /**
     * Get the resource name
     *
     * @return string
     */
    protected function getResourceName()
    {
        $name = ucwords($this->getResourceInput());

        if ($this->isByResource()) {
            return str_plural($name);
        }

        return $name;
    }

    /**
     * @return string
     */
    protected function getResourceInput()
    {
        return $this->argument('resource');
    }

    /**
     * Replace the value of the resource type string.
     *
     * @param mixed $stub
     * @return $this
     */
    protected function replaceResourceType(&$stub)
    {
        $resource = $this->getResourceName();
        $stub = str_replace('dummyResourceType', Str::dasherize($resource), $stub);

        return $this;
    }

    /**
     * Replace the value of the model class name.
     *
     * @param $stub
     * @return $this
     */
    protected function replaceRecord(&$stub)
    {
        $resource = $this->getResourceName();
        $stub = str_replace('DummyRecord', Str::classify(str_singular($resource)), $stub);

        return $this;
    }

    /**
     * Replace the value of the application namespace.
     *
     * @param $stub
     * @return $this
     */
    protected function replaceApplicationNamespace(&$stub)
    {
        $namespace = rtrim($this->laravel->getNamespace(), '\\');
        $stub = str_replace('DummyApplicationNamespace', $namespace, $stub);

        return $this;
    }

    /**
     * Replace the class name.
     *
     * @param $stub
     * @return $this
     */
    protected function replaceClassName(&$stub, $name)
    {
        $stub = $this->replaceClass($stub, $name);

        return $this;
    }

    /**
     * Get the stub for specific generator type
     *
     * @param string $implementationType
     * @return string
     */
    protected function getStubFor($implementationType)
    {
        return sprintf(
            '%s/%s/%s.stub',
            $this->stubsDirectory,
            $implementationType,
            Str::dasherize($this->type)
        );
    }

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

    /**
     * Determine whether a resource is eloquent or not
     *
     * @return boolean
     */
    protected function isEloquent()
    {
        if ($this->isIndependent) {
            return false;
        }

        if ($this->option('no-eloquent')) {
            return false;
        }

        return $this->option('eloquent') ?: $this->getApi()->isEloquent();
    }

    /**
     * @return Api
     */
    protected function getApi()
    {
        return $this->apiRepository->createApi($this->getApiName());
    }

    /**
     * @return string
     */
    protected function getApiName()
    {
        return $this->argument('api') ?: $this->laravel->make('json-api')->defaultApi();
    }
}


<?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\Codec\ChecksMediaTypes;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
use CloudCreativity\LaravelJsonApi\Document\ResourceObject;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Rules\AllowedFieldSets;
use CloudCreativity\LaravelJsonApi\Rules\AllowedFilterParameters;
use CloudCreativity\LaravelJsonApi\Rules\AllowedIncludePaths;
use CloudCreativity\LaravelJsonApi\Rules\AllowedPageParameters;
use CloudCreativity\LaravelJsonApi\Rules\AllowedSortParameters;
use CloudCreativity\LaravelJsonApi\Rules\DisallowedParameter;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

/**
 * Class AbstractValidators
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractValidators implements ValidatorFactoryInterface
{

    use ChecksMediaTypes;

    /**
     * Whether the resource supports client-generated ids.
     *
     * A boolean to indicate whether client-generated ids are accepted. If
     * null, this will be worked out based on whether there are any validation
     * rules for the resource's `id` member.
     *
     * @var bool|null
     */
    protected $clientIds;

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

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

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

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

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

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

    /**
     * The include paths a client is allowed to request.
     *
     * @var string[]|null
     *      the allowed paths, an empty array for none allowed, or null to allow all paths.
     */
    protected $allowedIncludePaths = [];

    /**
     * The sort field names a client is allowed send.
     *
     * @var string[]|null
     *      the allowed fields, an empty array for none allowed, or null to allow all fields.
     */
    protected $allowedSortParameters = [];

    /**
     * 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.
     *
     * Empty array = clients are not allowed to request filtering.
     * Null = clients can specify any filtering fields they want.
     *
     * @var string[]|null
     */
    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.
     *
     * Empty array = clients are not allowed to request paging.
     * Null = clients can specify any paging fields they want.
     *
     * @var string[]|null
     */
    protected $allowedPagingParameters = null;

    /**
     * What field sets the client is allowed to request per JSON API resource object type.
     *
     * Null = the client can specify any fields for any resource object type.
     * Empty array = the client cannot specify any fields for any resource object type (i.e. all denied.)
     * Non-empty array = configuration per JSON API resource object type. The key should be the type, the value should
     * be either null (all fields allowed for that type), empty array (no fields allowed for that type) or an array
     * of string values listing the allowed fields for that type.
     *
     * @return AllowedFieldSets
     */
    protected $allowedFieldSets = null;

    /**
     * Whether existing resource attributes should be validated on for an update.
     *
     * If this is set to false, the validator instance will not be provided with the
     * resource's existing attribute values when validating an update (PATCH) request.
     *
     * @var bool
     */
    protected $validateExisting = true;

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

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

    /**
     * Get resource validation rules.
     *
     * @param mixed|null $record
     *      the record being updated, or null if creating a resource.
     * @return mixed
     */
    abstract protected function rules($record = null): array;

    /**
     * Get query parameter validation rules.
     *
     * @return array
     */
    abstract protected function queryRules(): array;

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

    /**
     * @inheritDoc
     */
    public function supportsClientIds(): bool
    {
        if (is_bool($this->clientIds)) {
            return $this->clientIds;
        }

        return $this->clientIds = collect($this->rules())->has('id');
    }

    /**
     * @inheritDoc
     */
    public function create(array $document): ValidatorInterface
    {
        return $this->validatorForResource(
            $this->dataForCreate($document),
            $this->rules(),
            $this->messages(),
            $this->attributes()
        );
    }

    /**
     * @inheritDoc
     */
    public function update($record, array $document): ValidatorInterface
    {
        return $this->validatorForResource(
            $this->dataForUpdate($record, $document),
            $this->rules($record),
            $this->messages($record),
            $this->attributes($record)
        );
    }

    /**
     * @inheritDoc
     */
    public function delete($record): ?ValidatorInterface
    {
        if (!$rules = $this->deleteRules($record)) {
            return null;
        }

        return $this->validatorForDelete(
            $this->dataForDelete($record),
            $rules,
            $this->deleteMessages($record),
            $this->deleteAttributes($record)
        );
    }

    /**
     * @inheritDoc
     */
    public function modifyRelationship($record, string $field, array $document): ValidatorInterface
    {
        $data = $this->dataForRelationship($record, $field, $document);

        return $this->factory->createRelationshipValidator(
            ResourceObject::create($data),
            $this->relationshipRules($record, $field),
            $this->messages(),
            $this->attributes()
        );
    }

    /**
     * @inheritDoc
     */
    public function fetchManyQuery(array $params): ValidatorInterface
    {
        return $this->queryValidator($params);
    }

    /**
     * @inheritDoc
     */
    public function fetchQuery(array $params): ValidatorInterface
    {
        /**
         * Allow filter params, but not sort and page, for a resource GET.
         * Do not allow the `id` filter as it does not make sense as the request
         * is already limited by id.
         *
         * @see https://github.com/cloudcreativity/laravel-json-api/issues/218
         */
        return $this->validatorForQuery(
            $params,
            $this->queryRulesExcludingFilterId('sort', 'page'),
            $this->queryMessages(),
            $this->queryAttributes()
        );
    }

    /**
     * @inheritDoc
     */
    public function modifyQuery(array $params): ValidatorInterface
    {
        /** For modify resource requests, do not allow filter, sort and page. */
        return $this->queryValidator($params, ['filter', 'sort', 'page']);
    }

    /**
     * @inheritDoc
     */
    public function fetchRelatedQuery(array $params): ValidatorInterface
    {
        return $this->fetchManyQuery($params);
    }

    /**
     * @inheritDoc
     */
    public function fetchRelationshipQuery(array $params): ValidatorInterface
    {
        return $this->queryValidator($params);
    }

    /**
     * @inheritDoc
     */
    public function modifyRelationshipQuery(array $params): ValidatorInterface
    {
        return $this->fetchRelationshipQuery($params);
    }

    /**
     * Get custom messages for a resource object validator.
     *
     * @param mixed|null $record
     * @return array
     */
    protected function messages($record = null): array
    {
        return $this->messages;
    }

    /**
     * Get custom attributes for a resource object validator.
     *
     * @param mixed|null $record
     * @return array
     */
    protected function attributes($record = null): array
    {
        return $this->attributes;
    }

    /**
     * Get rules for a delete resource validator.
     *
     * @param $record
     * @return array|null
     *      the rules, or an empty value to indicate no validation.
     * @todo 2.0.0 make this abstract.
     */
    protected function deleteRules($record): ?array
    {
        return [];
    }

    /**
     * Get custom messages for a delete resource validator.
     *
     * @param $record
     * @return array
     */
    protected function deleteMessages($record): array
    {
        return $this->deleteMessages;
    }

    /**
     * Get custom attributes for a delete resource validator.
     *
     * @param $record
     * @return array
     */
    protected function deleteAttributes($record): array
    {
        return $this->deleteAttributes;
    }

    /**
     * Get validation data for creating a domain record.
     *
     * @param array $document
     * @return array
     */
    protected function dataForCreate(array $document): array
    {
        return $document['data'] ?? [];
    }

    /**
     * Get validation data for updating a domain record.
     *
     * The JSON API spec says:
     *
     * > If a request does not include all of the attributes for a resource,
     * > the server MUST interpret the missing attributes as if they were included
     * > with their current values. The server MUST NOT interpret missing
     * > attributes as null values.
     *
     * So that the validator has access to the current values of attributes, we
     * merge attributes provided by the client over the top of the existing attribute
     * values.
     *
     * @param mixed $record
     *      the record being updated.
     * @param array $document
     *      the JSON API document to validate.
     * @return array
     */
    protected function dataForUpdate($record, array $document): array
    {
        $resource = $document['data'] ?? [];

        if ($this->mustValidateExisting($record, $document)) {
            $resource['attributes'] = $this->extractAttributes(
                $record,
                $resource['attributes'] ?? []
            );

            $resource['relationships'] = $this->extractRelationships(
                $record,
                $resource['relationships'] ?? []
            );
        }

        return $resource;
    }

    /**
     * Get validation data for deleting a domain record.
     *
     * @param $record
     * @return array
     */
    protected function dataForDelete($record): array
    {
        $schema = $this->container->getSchema($record);

        return ResourceObject::create([
            'type' => $schema->getResourceType(),
            'id' => $schema->getId($record),
            'attributes' => $schema->getAttributes($record),
            'relationships' => collect($this->existingRelationships($record))->all(),
        ])->all();
    }

    /**
     * Should existing resource values be provided to the validator for an update request?
     *
     * Child classes can overload this method if they need to programmatically work out
     * if existing values must be provided to the validator instance for an update request.
     *
     * @param mixed $record
     *      the record being updated
     * @param array $document
     *      the JSON API document provided by the client.
     * @return bool
     */
    protected function mustValidateExisting($record, array $document): bool
    {
        return false !== $this->validateExisting;
    }

    /**
     * Extract attributes for a resource update.
     *
     * @param $record
     * @param array $new
     * @return array
     */
    protected function extractAttributes($record, array $new): array
    {
        return collect($this->existingAttributes($record))->merge($new)->all();
    }

    /**
     * Get any existing attributes for the provided record.
     *
     * @param $record
     * @return iterable
     */
    protected function existingAttributes($record): iterable
    {
        return $this->container->getSchema($record)->getAttributes($record);
    }

    /**
     * Extract relationships for a resource update.
     *
     * @param $record
     * @param array $new
     * @return array
     */
    protected function extractRelationships($record, array $new): array
    {
        return collect($this->existingRelationships($record))->merge($new)->all();
    }

    /**
     * Get any existing relationships for the provided record.
     *
     * As there is no reliable way for us to work this out (as we do not
     * know the relationship keys), child classes should overload this method
     * to add existing relationship data.
     *
     * @param $record
     * @return iterable
     */
    protected function existingRelationships($record): iterable
    {
        return [];
    }

    /**
     * Get validation data for modifying a relationship.
     *
     * @param mixed $record
     * @param string $field
     * @param array $document
     * @return array
     */
    protected function dataForRelationship($record, string $field, array $document): array
    {
        $schema = $this->container->getSchema($record);

        return [
            'type' => $schema->getResourceType(),
            'id' => $schema->getId($record),
            'relationships' => [
                $field => [
                    'data' => $document['data'],
                ],
            ],
        ];
    }

    /**
     * Get validation rules for a specified relationship field.
     *
     * @param mixed $record
     * @param string $field
     * @return array
     */
    protected function relationshipRules($record, string $field): array
    {
        return collect($this->rules($record))->filter(function ($v, $key) use ($field) {
            return Str::startsWith($key, $field);
        })->all();
    }

    /**
     * Create a validator for a JSON API resource object.
     *
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    protected function validatorForResource(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ): ValidatorInterface
    {
        return $this->factory->createResourceValidator(
            ResourceObject::create($data),
            $rules,
            $messages,
            $customAttributes
        );
    }

    /**
     * Create a validator for a delete request.
     *
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    protected function validatorForDelete(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ): ValidatorInterface
    {
        return $this->createValidator(
            $data,
            $rules,
            $messages,
            $customAttributes
        );
    }

    /**
     * Create a generic validator.
     *
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    protected function createValidator(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ): ValidatorInterface
    {
        return $this->factory->createDeleteValidator($data, $rules, $messages, $customAttributes);
    }

    /**
     * @return array
     */
    protected function allQueryRules(): array
    {
        return collect($this->defaultQueryRules())
            ->merge($this->queryRules())
            ->all();
    }

    /**
     * @return array
     */
    protected function defaultQueryRules(): array
    {
        return [
            'fields' => [
                'bail',
                'array',
                $this->allowedFieldSets(),
            ],
            'fields.*' => [
                'filled',
                'string',
            ],
            'filter' => [
                'bail',
                'array',
                $this->allowedFilteringParameters(),
            ],
            'include' => [
                'bail',
                'string',
                $this->allowedIncludePaths(),
            ],
            'page' => [
                'bail',
                'array',
                $this->allowedPagingParameters(),
            ],
            'sort' => [
                'bail',
                'string',
                $this->allowedSortParameters(),
            ],
        ];
    }

    /**
     * Get rules to disallow the provided keys.
     *
     * @param string ...$keys
     * @return Collection
     */
    protected function excluded(string ...$keys): Collection
    {
        return collect($keys)->mapWithKeys(function ($key) {
            return [$key => new DisallowedParameter($key)];
        });
    }

    /**
     * @param string ...$keys
     * @return array
     */
    protected function queryRulesWithout(string ...$keys): array
    {
        if (empty($keys)) {
            return $this->allQueryRules();
        }

        return collect($this->allQueryRules())->reject(function ($value, $key) use ($keys) {
            return Str::startsWith($key, $keys);
        })->merge($this->excluded(...$keys))->all();
    }

    /**
     * @param string ...$without
     * @return array
     */
    protected function queryRulesExcludingFilterId(string ...$without): array
    {
        $without[] = 'filter.id';
        $rules = $this->queryRulesWithout(...$without);

        $rules['filter'] = [
            'bail',
            'array',
            $this->allowedFilteringParameters()->forget('id')
        ];

        return $rules;
    }

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

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

    /**
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    protected function validatorForQuery(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ): ValidatorInterface {
        return $this->factory->createQueryValidator($data, $rules, $messages, $customAttributes);
    }

    /**
     * Get a validator for all query parameters.
     *
     * @param array $params
     * @param array|string|null $without
     * @return ValidatorInterface
     */
    protected function queryValidator(array $params, $without = null): ValidatorInterface
    {
        $without = (array) $without;

        return $this->validatorForQuery(
            $params,
            $this->queryRulesWithout(...$without),
            $this->queryMessages(),
            $this->queryAttributes()
        );
    }

    /**
     * Get a rule for the allowed include paths.
     *
     * @return AllowedIncludePaths
     */
    protected function allowedIncludePaths(): AllowedIncludePaths
    {
        return new AllowedIncludePaths($this->allowedIncludePaths);
    }

    /**
     * Get a rule for the allowed field sets.
     *
     * @return AllowedFieldSets
     */
    protected function allowedFieldSets(): AllowedFieldSets
    {
        return new AllowedFieldSets($this->allowedFieldSets);
    }

    /**
     * Get a rule for the allowed sort parameters.
     *
     * @return AllowedSortParameters
     */
    protected function allowedSortParameters(): AllowedSortParameters
    {
        return new AllowedSortParameters($this->allowedSortParameters);
    }

    /**
     * Get a rule for the allowed page parameters.
     *
     * @return AllowedPageParameters
     */
    protected function allowedPagingParameters(): AllowedPageParameters
    {
        return new AllowedPageParameters($this->allowedPagingParameters);
    }

    /**
     * Get a rule for the allowed filtering parameters.
     *
     * @return AllowedFilterParameters
     */
    protected function allowedFilteringParameters(): AllowedFilterParameters
    {
        return new AllowedFilterParameters($this->allowedFilteringParameters);
    }

}


<?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\Contracts\Object\RelationshipInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceIdentifierCollectionInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceIdentifierInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceObjectInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\AcceptRelatedResourceInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\RelationshipValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorErrorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Utils\ErrorsAwareTrait;
use CloudCreativity\LaravelJsonApi\Utils\Pointer as P;

/**
 * Class AbstractRelationshipValidator
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0 use classes in the `Validation` namespace instead.
 */
abstract class AbstractRelationshipValidator implements RelationshipValidatorInterface
{

    use ErrorsAwareTrait;

    /**
     * @var ValidatorErrorFactoryInterface
     */
    protected $errorFactory;

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

    /**
     * @var string[]|null
     *      if null, any types are supported.
     */
    private $expectedTypes;

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

    /**
     * @var AcceptRelatedResourceInterface|null
     */
    private $acceptable;

    /**
     * HasOneValidator constructor.
     *
     * @param ValidatorErrorFactoryInterface $errorFactory
     * @param StoreInterface $store
     * @param string|string[]|null $expectedType
     * @param bool $allowEmpty
     * @param AcceptRelatedResourceInterface|null $acceptable
     */
    public function __construct(
        ValidatorErrorFactoryInterface $errorFactory,
        StoreInterface $store,
        $expectedType,
        $allowEmpty = false,
        AcceptRelatedResourceInterface $acceptable = null
    ) {
        $this->errorFactory = $errorFactory;
        $this->store = $store;
        $this->expectedTypes = !is_null($expectedType) ? (array) $expectedType : null;
        $this->allowEmpty = $allowEmpty;
        $this->acceptable = $acceptable;
    }

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

    /**
     * @param ResourceIdentifierInterface $identifier
     * @return bool
     */
    protected function doesExist(ResourceIdentifierInterface $identifier)
    {
        return $this->store->exists($identifier);
    }

    /**
     * @param $type
     * @return bool
     */
    protected function isKnownType($type)
    {
        return $this->store->isType($type);
    }

    /**
     * @param $type
     * @return bool
     */
    protected function isSupportedType($type)
    {
        if (!is_array($this->expectedTypes)) {
            return true;
        }

        return in_array($type, $this->expectedTypes, true);
    }

    /**
     * Validate that a data member exists and it is either a has-one or a has-many relationship.
     *
     * @param RelationshipInterface $relationship
     * @param string|null $key
     * @return bool
     */
    protected function validateRelationship(RelationshipInterface $relationship, $key = null)
    {
        if (!$relationship->has(RelationshipInterface::DATA)) {
            $this->addError($this->errorFactory->memberRequired(
                RelationshipInterface::DATA,
                $key ? P::relationship($key) : P::data()
            ));
            return false;
        }

        if (!$relationship->isHasOne() && !$relationship->isHasMany()) {
            $this->addError($this->errorFactory->memberRelationshipExpected(
                RelationshipInterface::DATA,
                $key ? P::relationship($key) : P::data()
            ));
            return false;
        }

        if (!$this->validateEmpty($relationship, $key)) {
            return false;
        }

        return true;
    }

    /**
     * Is this a valid has-one relationship?
     *
     * @param RelationshipInterface $relationship
     * @param null $record
     * @param null $key
     * @param ResourceObjectInterface|null $resource
     * @return bool
     */
    protected function validateHasOne(
        RelationshipInterface $relationship,
        $record = null,
        $key = null,
        ResourceObjectInterface $resource = null
    ) {
        if (!$relationship->isHasOne()) {
            $this->addError($this->errorFactory->relationshipHasOneExpected($key));
            return false;
        }

        $identifier = $relationship->getData();

        if (!$identifier) {
            return true;
        }

        /** Validate the identifier */
        if (!$this->validateIdentifier($identifier, $key)) {
            return false;
        }

        /** If an identifier has been provided, the resource it references must exist. */
        if (!$this->validateExists($identifier, $key)) {
            return false;
        }

        /** If an identifier has been provided, is it acceptable for the relationship? */
        if (!$this->validateAcceptable($identifier, $record, $key, $resource)) {
            return false;
        }

        return true;
    }

    /**
     * Is this a valid has-many relationship?
     *
     * @param RelationshipInterface $relationship
     * @param null $record
     * @param null $key
     * @param ResourceObjectInterface|null $resource
     * @return bool
     */
    protected function validateHasMany(
        RelationshipInterface $relationship,
        $record = null,
        $key = null,
        ResourceObjectInterface $resource = null
    ) {
        if (!$relationship->isHasMany()) {
            $this->addError($this->errorFactory->relationshipHasManyExpected($key));
            return false;
        }

        $identifiers = $relationship->getIdentifiers();

        if (!$this->validateIdentifiers($identifiers, $record, $key, $resource)) {
            return false;
        }

        return true;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @param string|null $key
     * @return bool
     */
    protected function validateIdentifier(ResourceIdentifierInterface $identifier, $key = null)
    {
        $valid = $this->validateIdentifierType($identifier, $key);

        if (!$this->validateIdentifierId($identifier, $key)) {
            return false;
        }

        return $valid;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @param string|null $key
     * @return bool
     */
    protected function validateIdentifierType(ResourceIdentifierInterface $identifier, $key = null)
    {
        /** Must have a type */
        if (!$identifier->hasType()) {
            $this->addError($this->errorFactory->memberRequired(
                $identifier::TYPE,
                $key ? P::relationshipData($key) : P::data()
            ));
            return false;
        }

        $type = $identifier->get($identifier::TYPE);

        /** Must be a string */
        if (!is_string($type)) {
            $this->addError($this->errorFactory->memberStringExpected(
                $identifier::TYPE,
                $key ? P::relationshipData($key) : P::data()
            ));
            return false;
        }

        /** String must not be empty */
        if (empty($type)) {
            $this->addError($this->errorFactory->memberEmptyNotAllowed(
                $identifier::TYPE,
                $key ? P::relationshipData($key) : P::data()
            ));
            return false;
        }

        /** Check the submitted resource type is a known resource type */
        if (!$this->isKnownType($type)) {
            $this->addError($this->errorFactory->relationshipUnknownType($type, $key));
            return false;
        }

        /** Check type is valid for this relationship */
        if (!$this->isSupportedType($type)) {
            $this->addError($this->errorFactory->relationshipUnsupportedType($this->expectedTypes, $type, $key));
            return false;
        }

        return true;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @param string|null $key
     * @return bool
     */
    protected function validateIdentifierId(ResourceIdentifierInterface $identifier, $key = null)
    {
        /** Must have an id */
        if (!$identifier->hasId()) {
            $this->addError($this->errorFactory->memberRequired(
                $identifier::ID,
                $key ? P::relationshipId($key) : P::data()
            ));
            return false;
        }

        $id = $identifier->get($identifier::ID);

        /** Id must be a string */
        if (!is_string($id)) {
            $this->addError($this->errorFactory->memberStringExpected(
               ResourceIdentifierInterface::ID,
               $key ? P::relationshipId($key) : P::data()
            ));
            return false;
        }

        /** Id must not be empty */
        if (empty($id)) {
            $this->addError($this->errorFactory->memberEmptyNotAllowed(
                ResourceIdentifierInterface::ID,
                $key ? P::relationshipId($key) : P::data()
            ));
            return false;
        }

        return true;
    }

    /**
     * @param ResourceIdentifierCollectionInterface $identifiers
     * @param object|null $record
     * @param string|null $key
     * @param ResourceObjectInterface $resource
     * @return bool
     */
    protected function validateIdentifiers(
        ResourceIdentifierCollectionInterface $identifiers,
        $record = null,
        $key = null,
        ResourceObjectInterface $resource = null
    ) {
        /** @var ResourceIdentifierInterface $identifier */
        foreach ($identifiers as $identifier) {

            if (!$this->validateIdentifier($identifier, $key) || !$this->validateExists($identifier, $key)) {
                return false;
            }
        }

        /** @var ResourceIdentifierInterface $identifier */
        foreach ($identifiers as $identifier) {

            if (!$this->validateAcceptable($identifier, $record, $key, $resource)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @param string|null
     * @return bool
     */
    protected function validateExists(ResourceIdentifierInterface $identifier, $key = null)
    {
        if (!$this->doesExist($identifier)) {
            $this->addError($this->errorFactory->relationshipDoesNotExist($identifier, $key));
            return false;
        }

        return true;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @param object|null
     * @param string|null $key
     * @param ResourceObjectInterface|null $resource
     * @return bool
     */
    protected function validateAcceptable(
        ResourceIdentifierInterface $identifier,
        $record = null,
        $key = null,
        ResourceObjectInterface $resource = null
    ) {
        $result = ($this->acceptable) ? $this->acceptable->accept($identifier, $record, $key, $resource) : true;

        if (true !== $result) {
            $this->addErrors($this->errorFactory->relationshipNotAcceptable(
                $identifier,
                $key,
                !is_bool($result) ? $result : null
            ));
            return false;
        }

        return true;
    }

    /**
     * @param RelationshipInterface $relationship
     * @param string|null $key
     * @return bool
     */
    private function validateEmpty(RelationshipInterface $relationship, $key = null)
    {
        if ($relationship->isHasOne()) {
            $empty = !$relationship->hasIdentifier();
        } else {
            $empty = $relationship->getIdentifiers()->isEmpty();
        }

        if ($empty && !$this->isEmptyAllowed()) {
            $this->addError($this->errorFactory->relationshipEmptyNotAllowed($key));
            return false;
        }

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

use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;

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

    /**
     * @var QueryBuilder|EloquentBuilder|Relation
     */
    private $query;

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

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

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

    /**
     * CursorBuilder constructor.
     *
     * @param QueryBuilder|EloquentBuilder $query
     * @param string $column
     *      the column to use for the cursor.
     * @param string|null $key
     *      the key column that the before/after cursors related to.
     * @param bool $descending
     *      whether items are paged in descending order.
     */
    public function __construct($query, $column = null, $key = null, $descending = true)
    {
        if (!empty($query->orders)) {
            throw new RuntimeException('Cursor queries must not have an order applied.');
        }

        $this->query = $query;
        $this->column = $column ?: $this->guessColumn();
        $this->key = $key ?: $this->guessKey();
        $this->descending = $descending;
    }

    /**
     * @param Cursor $cursor
     * @param array $columns
     * @return CursorPaginator
     */
    public function paginate(Cursor $cursor, $columns = ['*'])
    {
        if ($cursor->isBefore()) {
            return $this->previous($cursor, $columns);
        }

        return $this->next($cursor, $columns);
    }

    /**
     * Get the next page.
     *
     * To calculate if there are more items in the list, we ask
     * for one more item than we actually need for the limit. We then
     * slice the results to remove this extra item.
     *
     * @param Cursor $cursor
     * @param $columns
     * @return CursorPaginator
     * @throws \OutOfRangeException
     *      if the cursor contains a before/after id that does not exist.
     */
    protected function next(Cursor $cursor, $columns)
    {
        if ($cursor->isAfter()) {
            $this->whereId($cursor->getAfter(), $this->descending ? '<' : '>');
        }

        $items = $this
            ->orderForNext()
            ->get($cursor->getLimit() + 1, $columns);

        $more = $items->count() > $cursor->getLimit();

        return new CursorPaginator(
            $items->slice(0, $cursor->getLimit()),
            $more,
            $cursor,
            $this->key
        );
    }

    /**
     * Get the previous page.
     *
     * To get the previous page, we need to sort in the opposite direction
     * (i.e. ascending rather than descending), then reverse the results
     * so that they are in the correct page order.
     *
     * The previous page always has-more items, because we know there is
     * at least one object ahead in the table - i.e. the one that was
     * provided as the before cursor.
     *
     * @param Cursor $cursor
     * @param $columns
     * @return CursorPaginator
     */
    protected function previous(Cursor $cursor, $columns)
    {
        $items = $this
            ->whereId($cursor->getBefore(), $this->descending ? '>' : '<')
            ->orderForPrevious()
            ->get($cursor->getLimit(), $columns)
            ->reverse()
            ->values();

        return new CursorPaginator($items, true, $cursor, $this->key);
    }

    /**
     * Add a where clause for the supplied id and operator.
     *
     * If we are paging on the key, then we only need one where clause - i.e.
     * on the key column.
     *
     * If we are paging on a column that is different than the key, we do not
     * assume that the column is unique. Therefore we add where clauses for
     * both the column plus then use the key column (which we expect to be
     * unique) to differentiate between any items that have the same value for
     * the non-unique column.
     *
     * @param $id
     * @param $operator
     * @return $this
     * @see https://stackoverflow.com/questions/38017054/mysql-cursor-based-pagination-with-multiple-columns
     */
    protected function whereId($id, $operator)
    {
        /** If we are paging on the key, we only need one where clause. */
        if ($this->isPagingOnKey()) {
            $this->query->where($this->key, $operator, $id);
            return $this;
        }

        $value = $this->getColumnValue($id);

        $this->query->where(
            $this->column, $operator . '=', $value
        )->where(function ($query) use ($id, $value, $operator) {
            /** @var QueryBuilder $query */
            $query->where($this->column, $operator, $value)->orWhere($this->key, $operator, $id);
        });

        return $this;
    }

    /**
     * Order items for a previous page query.
     *
     * A previous page query needs to retrieve items in the opposite
     * order from the desired order.
     *
     * @return $this
     */
    protected function orderForPrevious()
    {
        if ($this->descending) {
            $this->orderAsc();
        } else {
            $this->orderDesc();
        }

        return $this;
    }

    /**
     * Order items for a next page query.
     *
     * @return $this
     */
    protected function orderForNext()
    {
        if ($this->descending) {
            $this->orderDesc();
        } else {
            $this->orderAsc();
        }

        return $this;
    }

    /**
     * Order items in descending order.
     *
     * @return $this
     */
    protected function orderDesc()
    {
        $this->query->orderByDesc($this->column);

        if ($this->isNotPagingOnKey()) {
            $this->query->orderByDesc($this->key);
        }

        return $this;
    }

    /**
     * Order items in ascending order.
     *
     * @return $this
     */
    protected function orderAsc()
    {
        $this->query->orderBy($this->column);

        if ($this->isNotPagingOnKey()) {
            $this->query->orderBy($this->key);
        }

        return $this;
    }

    /**
     * @param $limit
     * @param mixed $columns
     * @return Collection
     */
    protected function get($limit, $columns)
    {
        return $this->query->take($limit)->get($columns);
    }

    /**
     * Get the column value for the provided id.
     *
     * @param $id
     * @return mixed
     * @throws \OutOfRangeException
     *      if the id does not exist.
     */
    protected function getColumnValue($id)
    {
        $value = $this
            ->getQuery() // we want the raw DB value, not the Model value as that can be mutated.
            ->where($this->key, $id)
            ->value($this->column);

        if (is_null($value)) {
            throw new \OutOfRangeException("Cursor key {$id} does not exist or has a null value.");
        }

        return $value;
    }

    /**
     * Get a base query builder instance.
     *
     * @return QueryBuilder
     */
    protected function getQuery()
    {
        if (!$this->query instanceof QueryBuilder) {
            return clone $this->query->getQuery();
        }

        return clone $this->query;
    }

    /**
     * Are we paging using the key column?
     *
     * @return bool
     */
    protected function isPagingOnKey()
    {
        return $this->column === $this->key;
    }

    /**
     * Are we not paging on the key column?
     *
     * @return bool
     */
    protected function isNotPagingOnKey()
    {
        return !$this->isPagingOnKey();
    }

    /**
     * Guess the column to use for the cursor.
     *
     * @return string
     */
    private function guessColumn()
    {
        if ($this->query instanceof EloquentBuilder || $this->query instanceof Relation) {
            return $this->query->getModel()->getCreatedAtColumn();
        }

        return Model::CREATED_AT;
    }

    /**
     * Guess the key to use for the cursor.
     *
     * @return string
     */
    private function guessKey()
    {
        if ($this->query instanceof EloquentBuilder || $this->query instanceof Relation) {
            return $this->query->getModel()->getRouteKeyName();
        }

        return '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\Utils;

use CloudCreativity\LaravelJsonApi\Exceptions\DocumentRequiredException;
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidJsonException;
use Illuminate\Support\Str as IlluminateStr;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Http\Headers\MediaType;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class Helpers
{

    /**
     * Decode a JSON string.
     *
     * @param string $content
     * @param bool $assoc
     * @param int $depth
     * @param int $options
     * @return object|array
     * @throws InvalidJsonException
     */
    public static function decode($content, $assoc = false, $depth = 512, $options = 0)
    {
        $decoded = \json_decode($content, $assoc, $depth, $options);

        if (JSON_ERROR_NONE !== json_last_error()) {
            throw InvalidJsonException::create();
        }

        if (!$assoc && !is_object($decoded)) {
            throw new DocumentRequiredException();
        }

        if ($assoc && !is_array($decoded)) {
            throw new InvalidJsonException(null, 'JSON is not an array.');
        }

        return $decoded;
    }

    /**
     * Does the HTTP request contain body content?
     *
     * "The presence of a message-body in a request is signaled by the inclusion of a Content-Length or
     * Transfer-Encoding header field in the request's message-headers."
     * https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
     *
     * However, some browsers send a Content-Length header with an empty string for e.g. GET requests
     * without any message-body. Therefore rather than checking for the existence of a Content-Length
     * header, we will allow an empty value to indicate that the request does not contain body.
     *
     * @param RequestInterface|SymfonyRequest $request
     * @return bool
     */
    public static function doesRequestHaveBody($request)
    {
        if (self::hasHeader($request, 'Transfer-Encoding')) {
            return true;
        };

        if (1 > self::getHeader($request, 'Content-Length')) {
            return false;
        }

        return true;
    }

    /**
     * Does the HTTP response contain body content?
     *
     * "For response messages, whether or not a message-body is included with a message is dependent
     * on both the request method and the response status code (section 6.1.1). All responses to the
     * HEAD request method MUST NOT include a message-body, even though the presence of entity-header
     * fields might lead one to believe they do. All 1xx (informational), 204 (no content), and 304
     * (not modified) responses MUST NOT include a message-body. All other responses do include a
     * message-body, although it MAY be of zero length."
     * https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
     *
     * @param RequestInterface|SymfonyRequest $request
     * @param ResponseInterface|SymfonyResponse $response
     * @return bool
     */
    public static function doesResponseHaveBody($request, $response)
    {
        if ('HEAD' === strtoupper($request->getMethod())) {
            return false;
        }

        $status = $response->getStatusCode();

        if ((100 <= $status && 200 > $status) || 204 === $status || 304 === $status) {
            return false;
        }

        if (self::hasHeader($response, 'Transfer-Encoding')) {
            return true;
        };

        if (!$contentLength = self::getHeader($response, 'Content-Length')) {
            return false;
        }

        return 0 < $contentLength[0];
    }

    /**
     * Does the request want JSON API content?
     *
     * @param SymfonyRequest $request
     * @return bool
     */
    public static function wantsJsonApi($request)
    {
        $acceptable = $request->getAcceptableContentTypes();

        return isset($acceptable[0]) && IlluminateStr::contains($acceptable[0], MediaType::JSON_API_SUB_TYPE);
    }

    /**
     * Has the request sent JSON API content?
     *
     * @param SymfonyRequest $request
     * @return bool
     */
    public static function isJsonApi($request)
    {
        return IlluminateStr::contains($request->headers->get('Content-Type'), MediaType::JSON_API_SUB_TYPE);
    }

    /**
     * Get the most applicable HTTP status code.
     *
     * When a server encounters multiple problems for a single request, the most generally applicable HTTP error
     * code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate for multiple
     * 4xx errors or 500 Internal Server Error might be appropriate for multiple 5xx errors.
     *
     * @param iterable|ErrorInterface $errors
     * @param int $default
     * @return int
     * @see https://jsonapi.org/format/#errors
     */
    public static function httpErrorStatus($errors, int $default = SymfonyResponse::HTTP_BAD_REQUEST): int
    {
        if ($errors instanceof ErrorInterface) {
            $errors = [$errors];
        }

        $statuses = collect($errors)->reject(function (ErrorInterface $error) {
            return is_null($error->getStatus());
        })->map(function (ErrorInterface $error) {
            return (int) $error->getStatus();
        })->unique();

        if (2 > count($statuses)) {
            return $statuses->first() ?: $default;
        }

        $only4xx = $statuses->every(function (int $status) {
            return 400 <= $status && 499 >= $status;
        });

        return $only4xx ? SymfonyResponse::HTTP_BAD_REQUEST : SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR;
    }

    /**
     * @param MessageInterface|SymfonyRequest|SymfonyResponse $message
     * @param $key
     * @return mixed
     */
    private static function getHeader($message, $key)
    {
        if ($message instanceof MessageInterface) {
            return $message->getHeader($key)[0] ?? null;
        }

        return $message->headers->get($key);
    }

    /**
     * @param MessageInterface|SymfonyRequest|SymfonyResponse $message
     * @param $key
     * @return mixed
     */
    private static function hasHeader($message, $key)
    {
        if ($message instanceof MessageInterface) {
            return $message->hasHeader($key);
        }

        return $message->headers->has($key);
    }

}


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

use CloudCreativity\LaravelJsonApi\Api\AbstractProvider;
use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Api\LinkGenerator;
use CloudCreativity\LaravelJsonApi\Api\ResourceProvider;
use CloudCreativity\LaravelJsonApi\Api\Url;
use CloudCreativity\LaravelJsonApi\Api\UrlGenerator;
use CloudCreativity\LaravelJsonApi\Client\ClientSerializer;
use CloudCreativity\LaravelJsonApi\Client\GuzzleClient;
use CloudCreativity\LaravelJsonApi\Codec\Codec;
use CloudCreativity\LaravelJsonApi\Codec\Decoding;
use CloudCreativity\LaravelJsonApi\Codec\Encoding;
use CloudCreativity\LaravelJsonApi\Container;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Encoder\SerializerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Factories\FactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Http\ContentNegotiatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Repositories\ErrorRepositoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Resolver\ResolverInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\DocumentValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\QueryValidatorInterface;
use CloudCreativity\LaravelJsonApi\Document\ResourceObject;
use CloudCreativity\LaravelJsonApi\Encoder\Encoder;
use CloudCreativity\LaravelJsonApi\Encoder\Parameters\EncodingParameters;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Http\ContentNegotiator;
use CloudCreativity\LaravelJsonApi\Http\Headers\RestrictiveHeadersChecker;
use CloudCreativity\LaravelJsonApi\Http\Query\ValidationQueryChecker;
use CloudCreativity\LaravelJsonApi\Http\Responses\ErrorResponse;
use CloudCreativity\LaravelJsonApi\Http\Responses\Responses;
use CloudCreativity\LaravelJsonApi\Object\Document;
use CloudCreativity\LaravelJsonApi\Pagination\Page;
use CloudCreativity\LaravelJsonApi\Repositories\ErrorRepository;
use CloudCreativity\LaravelJsonApi\Resolver\ResolverFactory;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use CloudCreativity\LaravelJsonApi\Store\Store;
use CloudCreativity\LaravelJsonApi\Utils\Replacer;
use CloudCreativity\LaravelJsonApi\Validation;
use CloudCreativity\LaravelJsonApi\Validation\ErrorTranslator;
use CloudCreativity\LaravelJsonApi\Validators\ValidatorErrorFactory;
use CloudCreativity\LaravelJsonApi\Validators\ValidatorFactory;
use Illuminate\Contracts\Container\Container as IlluminateContainer;
use Illuminate\Contracts\Routing\UrlGenerator as IlluminateUrlGenerator;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Contracts\Validation\Factory as ValidatorFactoryContract;
use Illuminate\Contracts\Validation\Validator;
use Neomerx\JsonApi\Contracts\Codec\CodecMatcherInterface;
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
use Neomerx\JsonApi\Contracts\Schema\ContainerInterface as SchemaContainerInterface;
use Neomerx\JsonApi\Encoder\EncoderOptions;
use Neomerx\JsonApi\Factories\Factory as BaseFactory;
use Psr\Http\Message\RequestInterface as PsrRequest;
use Psr\Http\Message\ResponseInterface as PsrResponse;
use function CloudCreativity\LaravelJsonApi\http_contains_body;
use function CloudCreativity\LaravelJsonApi\json_decode;

/**
 * Class Factory
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Factory extends BaseFactory implements FactoryInterface
{

    /**
     * @var IlluminateContainer
     */
    protected $container;

    /**
     * Factory constructor.
     *
     * @param IlluminateContainer $container
     */
    public function __construct(IlluminateContainer $container)
    {
        parent::__construct();
        $this->container = $container;
    }

    /**
     * Create a resolver.

     * @param string $apiName
     * @param array $config
     * @return ResolverInterface
     */
    public function createResolver($apiName, array $config)
    {
        $factoryName = isset($config['resolver']) ? $config['resolver'] : ResolverFactory::class;
        $factory = $this->container->make($factoryName);

        if ($factory instanceof ResolverInterface) {
            return $factory;
        }

        if (!is_callable($factory)) {
            throw new RuntimeException("Factory {$factoryName} cannot be invoked.");
        }

        $resolver = $factory($apiName, $config);

        if (!$resolver instanceof ResolverInterface) {
            throw new RuntimeException("Factory {$factoryName} did not create a resolver instance.");
        }

        return $resolver;
    }

    /**
     * @inheritdoc
     */
    public function createHeadersChecker(CodecMatcherInterface $codecMatcher)
    {
        return new RestrictiveHeadersChecker($codecMatcher, json_api()->getErrors());
    }

    /**
     * @inheritdoc
     */
    public function createExtendedContainer(ResolverInterface $resolver)
    {
        return new Container($this->container, $resolver);
    }

    /**
     * @inheritdoc
     */
    public function createEncoder(SchemaContainerInterface $container, EncoderOptions $encoderOptions = null)
    {
        return $this->createSerializer($container, $encoderOptions);
    }

    /**
     * @inheritDoc
     */
    public function createSerializer(SchemaContainerInterface $container, EncoderOptions $encoderOptions = null)
    {
        $encoder = new Encoder($this, $container, $encoderOptions);
        $encoder->setLogger($this->logger);

        return $encoder;
    }

    /**
     * @inheritDoc
     */
    public function createErrorResponse($errors, $defaultHttpCode, array $headers = [])
    {
        return new ErrorResponse($errors, $defaultHttpCode, $headers);
    }

    /**
     * @inheritDoc
     */
    public function createDocumentObject(PsrRequest $request, PsrResponse $response = null)
    {
        if (!http_contains_body($request, $response)) {
            return null;
        }

        return new Document(json_decode($response ? $response->getBody() : $request->getBody()));
    }

    /**
     * @inheritDoc
     */
    public function createClient($httpClient, SchemaContainerInterface $container, SerializerInterface $encoder)
    {
        return new GuzzleClient(
            $httpClient,
            $container,
            new ClientSerializer($encoder, $this)
        );
    }

    /**
     * @inheritdoc
     */
    public function createStore(ContainerInterface $container)
    {
        return new Store($container);
    }

    /**
     * @inheritDoc
     */
    public function createErrorRepository(array $errors)
    {
        $repository = new ErrorRepository($this->createReplacer());
        $repository->configure($errors);

        return $repository;
    }

    /**
     * @inheritDoc
     */
    public function createReplacer()
    {
        return new Replacer();
    }

    /**
     * @inheritDoc
     */
    public function createExtendedQueryChecker(
        $allowUnrecognized = false,
        array $includePaths = null,
        array $fieldSetTypes = null,
        array $sortParameters = null,
        array $pagingParameters = null,
        array $filteringParameters = null,
        QueryValidatorInterface $validator = null
    ) {
        $checker = $this->createQueryChecker(
            $allowUnrecognized,
            $includePaths,
            $fieldSetTypes,
            $sortParameters,
            $pagingParameters,
            $filteringParameters
        );

        return new ValidationQueryChecker($checker, $validator);
    }

    /**
     * @inheritDoc
     */
    public function createPage(
        $data,
        LinkInterface $first = null,
        LinkInterface $previous = null,
        LinkInterface $next = null,
        LinkInterface $last = null,
        $meta = null,
        $metaKey = null
    ) {
        return new Page($data, $first, $previous, $next, $last, $meta, $metaKey);
    }

    /**
     * @inheritdoc
     */
    public function createValidatorFactory(ErrorRepositoryInterface $errors, StoreInterface $store)
    {
        /** @var ValidatorFactoryContract $laravelFactory */
        $laravelFactory = $this->container->make(ValidatorFactoryContract::class);

        return new ValidatorFactory($this->createValidatorErrorFactory($errors), $store, $laravelFactory);
    }

    /**
     * @param $fqn
     * @return ResourceProvider
     */
    public function createResourceProvider($fqn)
    {
        $provider = $this->container->make($fqn);

        if (!$provider instanceof AbstractProvider) {
            throw new RuntimeException("Expecting $fqn to resolve to a resource provider instance.");
        }

        return $provider;
    }

    /**
     * Create a response factory.
     *
     * @param Api $api
     * @return Responses
     */
    public function createResponseFactory(Api $api)
    {
        return new Responses(
            $this,
            $api,
            $this->container->make(Route::class),
            $this->container->make('json-api.exceptions')
        );
    }

    /**
     * @param Url $url
     * @return UrlGenerator
     */
    public function createUrlGenerator(Url $url)
    {
        $generator = $this->container->make(IlluminateUrlGenerator::class);

        return new UrlGenerator($generator, $url);
    }

    /**
     * @param UrlGenerator $urls
     * @return LinkGenerator
     */
    public function createLinkGenerator(UrlGenerator $urls)
    {
        $generator = $this->container->make(IlluminateUrlGenerator::class);

        return new LinkGenerator($this, $urls, $generator);
    }

    /**
     * @inheritdoc
     */
    public function createQueryParameters(
        $includePaths = null,
        array $fieldSets = null,
        $sortParameters = null,
        array $pagingParameters = null,
        array $filteringParameters = null,
        array $unrecognizedParams = null
    ) {
        return new EncodingParameters(
            $includePaths,
            $fieldSets,
            $sortParameters,
            $pagingParameters,
            $filteringParameters,
            $unrecognizedParams
        );
    }

    /**
     * Create a validator to check that a resource document complies with the JSON API specification.
     *
     * @param object $document
     * @param string $expectedType
     * @param bool $clientIds
     *      whether client ids are supported.
     * @return Validation\Spec\CreateResourceValidator
     */
    public function createNewResourceDocumentValidator($document, $expectedType, $clientIds)
    {
        $store = $this->container->make(StoreInterface::class);
        $errors = $this->createErrorTranslator();

        return new Validation\Spec\CreateResourceValidator(
            $store,
            $errors,
            $document,
            $expectedType,
            $clientIds
        );
    }

    /**
     * Create a validator to check that a resource document complies with the JSON API specification.
     *
     * @param object $document
     * @param string $expectedType
     * @param string $expectedId
     * @return Validation\Spec\UpdateResourceValidator
     */
    public function createExistingResourceDocumentValidator($document, $expectedType, $expectedId)
    {
        $store = $this->container->make(StoreInterface::class);
        $errors = $this->createErrorTranslator();

        return new Validation\Spec\UpdateResourceValidator(
            $store,
            $errors,
            $document,
            $expectedType,
            $expectedId
        );
    }

    /**
     * Create a validator to check that a relationship document complies with the JSON API specification.
     *
     * @param object $document
     * @return DocumentValidatorInterface
     */
    public function createRelationshipDocumentValidator($document)
    {
        return new Validation\Spec\RelationValidator(
            $this->container->make(StoreInterface::class),
            $this->createErrorTranslator(),
            $document
        );
    }

    /**
     * Create an error translator.
     *
     * @return ErrorTranslator
     */
    public function createErrorTranslator()
    {
        return new ErrorTranslator(
            $this->container->make(Translator::class)
        );
    }

    /**
     * Create a content negotiator.
     *
     * @return ContentNegotiatorInterface
     */
    public function createContentNegotiator()
    {
        return new ContentNegotiator($this);
    }

    /**
     * @param ContainerInterface $container
     * @param Encoding $encoding
     * @param Decoding|null $decoding
     * @return Codec
     */
    public function createCodec(ContainerInterface $container, Encoding $encoding, ?Decoding $decoding)
    {
        return new Codec($this, $container, $encoding, $decoding);
    }

    /**
     * Create a Laravel validator that has JSON API error objects.
     *
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @param \Closure|null $callback
     *       a closure for creating an error, that will be bound to the error translator.
     * @return ValidatorInterface
     */
    public function createValidator(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = [],
        \Closure $callback = null
    ): ValidatorInterface
    {
        $translator = $this->createErrorTranslator();

        return new Validation\Validator(
            $this->makeValidator($data, $rules, $messages, $customAttributes),
            $translator,
            $callback
        );
    }

    /**
     * Create a JSON API resource object validator.
     *
     * @param ResourceObject $resource
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    public function createResourceValidator(
        ResourceObject $resource,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ) {
        return $this->createValidator(
            $resource->all(),
            $rules,
            $messages,
            $customAttributes,
            function ($key, $detail, $failed) use ($resource) {
                return $this->invalidResource(
                    $resource->pointer($key, '/data'),
                    $detail,
                    $failed
                );
            }
        );
    }

    /**
     * Create a JSON API relationship validator.
     *
     * @param ResourceObject $resource
     *      the resource object, containing only the relationship field.
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    public function createRelationshipValidator(
        ResourceObject $resource,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ) {
        return $this->createValidator(
            $resource->all(),
            $rules,
            $messages,
            $customAttributes,
            function ($key, $detail, $failed) use ($resource) {
                return $this->invalidResource(
                    $resource->pointerForRelationship($key, '/data'),
                    $detail,
                    $failed
                );
            }
        );
    }

    /**
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    public function createDeleteValidator(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ) {
        return $this->createValidator(
            $data,
            $rules,
            $messages,
            $customAttributes,
            function ($key, $detail) {
                return $this->resourceCannotBeDeleted($detail);
            }
        );
    }

    /**
     * Create a query validator.
     *
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return ValidatorInterface
     */
    public function createQueryValidator(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ) {
        return $this->createValidator(
            $data,
            $rules,
            $messages,
            $customAttributes,
            function ($key, $detail, $failed) {
                return $this->invalidQueryParameter($key, $detail, $failed);
            }
        );
    }

    /**
     * @param ErrorRepositoryInterface $errors
     * @return ValidatorErrorFactory
     * @deprecated 2.0.0
     */
    protected function createValidatorErrorFactory(ErrorRepositoryInterface $errors)
    {
        return new ValidatorErrorFactory($errors);
    }

    /**
     * @param array $data
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return Validator
     */
    protected function makeValidator(
        array $data,
        array $rules,
        array $messages = [],
        array $customAttributes = []
    ) {
        return $this->container
            ->make(ValidatorFactoryContract::class)
            ->make($data, $rules, $messages, $customAttributes);
    }
}


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

use CloudCreativity\LaravelJsonApi\Routing\ResourceRegistrar;
use CloudCreativity\LaravelJsonApi\Routing\RouteName;
use Illuminate\Contracts\Routing\UrlGenerator as IlluminateUrlGenerator;

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

    /**
     * @var IlluminateUrlGenerator
     */
    private $generator;

    /**
     * @var Url
     */
    private $url;

    /**
     * UrlGenerator constructor.
     *
     * @param IlluminateUrlGenerator $generator
     * @param Url $url
     */
    public function __construct(IlluminateUrlGenerator $generator, Url $url)
    {
        $this->generator = $generator;
        $this->url = $url;
    }

    /**
     * Get a link to the index of a resource type.
     *
     * @param $resourceType
     * @param array $queryParams
     * @return string
     */
    public function index($resourceType, array $queryParams = [])
    {
        return $this->route(RouteName::index($resourceType), $queryParams);
    }

    /**
     * Get a link to create a resource object.
     *
     * @param $resourceType
     * @param array $queryParams
     * @return string
     */
    public function create($resourceType, array $queryParams = [])
    {
        return $this->route(RouteName::create($resourceType), $queryParams);
    }

    /**
     * Get a link to read a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array $queryParams
     * @return string
     */
    public function read($resourceType, $id, array $queryParams = [])
    {
        return $this->resource(RouteName::read($resourceType), $id, $queryParams);
    }

    /**
     * Get a link to update a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array $queryParams
     * @return string
     */
    public function update($resourceType, $id, array $queryParams = [])
    {
        return $this->resource(RouteName::update($resourceType), $id, $queryParams);
    }

    /**
     * Get a link to delete a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array $queryParams
     * @return string
     */
    public function delete($resourceType, $id, array $queryParams = [])
    {
        return $this->resource(RouteName::delete($resourceType), $id, $queryParams);
    }

    /**
     * Get a link to a resource object's related resource.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array $queryParams
     * @return string
     */
    public function relatedResource($resourceType, $id, $relationshipKey, array $queryParams = [])
    {
        return $this->resource(RouteName::related($resourceType, $relationshipKey), $id, $queryParams);
    }

    /**
     * Get a link to read a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array $queryParams
     * @return string
     */
    public function readRelationship($resourceType, $id, $relationshipKey, array $queryParams = [])
    {
        $name = RouteName::readRelationship($resourceType, $relationshipKey);

        return $this->resource($name, $id, $queryParams);
    }

    /**
     * Get a link to replace a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array $queryParams
     * @return string
     */
    public function replaceRelationship($resourceType, $id, $relationshipKey, array $queryParams = [])
    {
        $name = RouteName::replaceRelationship($resourceType, $relationshipKey);

        return $this->resource($name, $id, $queryParams);
    }

    /**
     * Get a link to add to a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array $queryParams
     * @return string
     */
    public function addRelationship($resourceType, $id, $relationshipKey, array $queryParams = [])
    {
        $name = RouteName::addRelationship($resourceType, $relationshipKey);

        return $this->resource($name, $id, $queryParams);
    }

    /**
     * Get a link to remove from a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array $queryParams
     * @return string
     */
    public function removeRelationship($resourceType, $id, $relationshipKey, array $queryParams = [])
    {
        $name = RouteName::removeRelationship($resourceType, $relationshipKey);

        return $this->resource($name, $id, $queryParams);
    }

    /**
     * @param $name
     * @param array $parameters
     * @return string
     */
    private function route($name, $parameters = [])
    {
        $name = $this->url->getName() . $name;

        return $this->generator->route($name, $parameters, true);
    }

    /**
     * @param $name
     * @param $id
     * @param array $parameters
     * @return string
     */
    private function resource($name, $id, $parameters = [])
    {
        $parameters[ResourceRegistrar::PARAM_RESOURCE_ID] = $id;

        return $this->route($name, $parameters);
    }
}


<?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.
 */

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateClientJobsTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('json_api_client_jobs', function (Blueprint $table) {
            $table->uuid('uuid')->primary();
            $table->timestamps();
            $table->string('api');
            $table->string('resource_type');
            $table->string('resource_id')->nullable();
            $table->timestamp('completed_at')->nullable();
            $table->integer('attempts')->default(0);
            $table->integer('timeout')->nullable();
            $table->timestamp('timeout_at')->nullable();
            $table->integer('tries')->nullable();
            $table->boolean('failed')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('json_api_client_jobs');
    }
}


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

use CloudCreativity\LaravelJsonApi\Contracts\Resolver\ResolverInterface;

/**
 * Class AbstractResolver
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractResolver implements ResolverInterface
{

    /**
     * @var array
     */
    protected $resources;

    /**
     * @var array
     */
    protected $types;

    /**
     * Convert the provided unit name and resource type into a fully qualified namespace.
     *
     * @param string $unit
     *      the JSON API unit name: Adapter, Authorizer, ContentNegotiator, Schema, Validators
     * @param $resourceType
     *      the JSON API resource type.
     * @return string
     */
    abstract protected function resolve($unit, $resourceType);

    /**
     * AbstractResolver constructor.
     *
     * @param array $resources
     */
    public function __construct(array $resources)
    {
        $this->resources = $resources;
        $this->types = $this->flip($resources);
    }

    /**
     * @inheritDoc
     */
    public function isType($type)
    {
        return isset($this->types[$type]);
    }

    /**
     * @inheritdoc
     */
    public function getType($resourceType)
    {
        if (!isset($this->resources[$resourceType])) {
            return null;
        }

        return $this->resources[$resourceType];
    }

    /**
     * @inheritDoc
     */
    public function getAllTypes()
    {
        return array_keys($this->types);
    }

    /**
     * @inheritDoc
     */
    public function isResourceType($resourceType)
    {
        return isset($this->resources[$resourceType]);
    }

    /**
     * @inheritdoc
     */
    public function getResourceType($type)
    {
        if (!isset($this->types[$type])) {
            return null;
        }

        return $this->types[$type];
    }

    /**
     * @inheritDoc
     */
    public function getAllResourceTypes()
    {
        return array_keys($this->resources);
    }

    /**
     * @inheritdoc
     */
    public function getSchemaByType($type)
    {
        $resourceType = $this->getResourceType($type);

        return $resourceType ? $this->getSchemaByResourceType($resourceType) : null;
    }

    /**
     * @inheritdoc
     */
    public function getSchemaByResourceType($resourceType)
    {
        return $this->resolve('Schema', $resourceType);
    }

    /**
     * @inheritdoc
     */
    public function getAdapterByType($type)
    {
        $resourceType = $this->getResourceType($type);

        return $resourceType ? $this->getAdapterByResourceType($resourceType) : null;
    }

    /**
     * @inheritdoc
     */
    public function getAdapterByResourceType($resourceType)
    {
        return $this->resolve('Adapter', $resourceType);
    }

    /**
     * @inheritdoc
     */
    public function getAuthorizerByType($type)
    {
        $resourceType = $this->getResourceType($type);

        return $resourceType ? $this->getAuthorizerByResourceType($resourceType) : null;
    }

    /**
     * @inheritdoc
     */
    public function getAuthorizerByResourceType($resourceType)
    {
        return $this->resolve('Authorizer', $resourceType);
    }

    /**
     * @inheritDoc
     */
    public function getAuthorizerByName($name)
    {
        return $this->resolveName('Authorizer', $name);
    }

    /**
     * @inheritDoc
     */
    public function getContentNegotiatorByResourceType($resourceType)
    {
        return $this->resolve('ContentNegotiator', $resourceType);
    }

    /**
     * @inheritDoc
     */
    public function getContentNegotiatorByName($name)
    {
        return $this->resolveName('ContentNegotiator', $name);
    }

    /**
     * @inheritdoc
     */
    public function getValidatorsByType($type)
    {
        $resourceType = $this->getResourceType($type);

        return $resourceType ? $this->getValidatorsByResourceType($resourceType) : null;
    }

    /**
     * @inheritdoc
     */
    public function getValidatorsByResourceType($resourceType)
    {
        return $this->resolve('Validators', $resourceType);
    }

    /**
     * Resolve a name that is not a resource type.
     *
     * @param $unit
     * @param $name
     * @return string
     */
    protected function resolveName($unit, $name)
    {
        return $this->resolve($unit, $name);
    }

    /**
     * Key the resource array by domain record type.
     *
     * @param array $resources
     * @return array
     */
    private function flip(array $resources)
    {
        $all = [];

        foreach ($resources as $resourceType => $types) {
            foreach ((array) $types as $type) {
                $all[$type] = $resourceType;
            }
        }

        return $all;
    }
}