diff --git a/ckan_connect.info.yml b/ckan_connect.info.yml old mode 100644 new mode 100755 diff --git a/ckan_connect.links.menu.yml b/ckan_connect.links.menu.yml old mode 100644 new mode 100755 diff --git a/ckan_connect.module b/ckan_connect.module old mode 100644 new mode 100755 diff --git a/ckan_connect.permissions.yml b/ckan_connect.permissions.yml old mode 100644 new mode 100755 diff --git a/ckan_connect.routing.yml b/ckan_connect.routing.yml old mode 100644 new mode 100755 diff --git a/ckan_connect.services.yml b/ckan_connect.services.yml old mode 100644 new mode 100755 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..24b7a2e --- /dev/null +++ b/readme.md @@ -0,0 +1,152 @@ +Drupal CKAN Connect +=================== + +This is a developer module that provides a client for the [CKAN action API](http://docs.ckan.org/en/latest/api/index.html#action-api-reference). + +Usage +----- + +Every API request has three steps: +1. Create a new object related to the CKAN entity or service you wish to interact with. +2. Set any required properties on the object. +3. Use the `ckan_connect.client` service to make the API call using your newly created object. + +### Service calls + +> Technically, service calls can be used for any request to the CKAN API. They do not do any parameter checking and they +all work exactly the same way. If you want parameter validation and automatic handling of IDs where required for entity +actions, use the [CRUD-like objects](#crud-like-objects). + +A service call is easy to use as long as you know exactly what the API is expecting. Just give it the full action name +such as `package_search` or `organization_autocomplete` and any parameters it requires. Parameters can be given in the +construction method, or separately via `CkanApiInterface::setParameters($parameters)`. + +```php + 'search terms', +]; +$package_search->setParameters($parameters); +$client = \Drupal::service('ckan_connect.client'); +$search_result = $client->action($package_search); + +// OR + +$package_search = new CkanRequest('package_search', ['q' => 'search terms']); +$client = \Drupal::service('ckan_connect.client'); +$search_result = $client->action($package_search); + +``` + +That's it. The client uses the API URL and API key you've stored in settings and handles any errors that are thrown by +guzzle or the API itself so that your application doesn't crash. + +### CRUD-like objects + +Where the CKAN endpoints can be grouped together into a set of related functions, they are bunched into one of these +object types: + +- CRUD objects have endpoint actions for Create, Read (`show`), Update and Delete, they are in `\src\Ckan\Crud\*`. They + also allow a `list` action. +- Patchable objects have an additional `patch` action on top of the ones allowed by CRUD objects and are in + `\src\Ckan\Patchable\*`. The `patch` action is like `update` except that it only updates the values provided in the + call, rather than overwriting all values the way `update` does. +- Member objects are basically entity references and can only created or deleted (a subset of CRUD). These are in + `\src\Ckan\Member\*`. + +When creating a new CRUD-like object, note that the creation call expects an ID as a string. You can leave this string +empty if the object is going to be used for a `create` or `list` action but it expects a full CKAN ID for any other +action. + +```php + 'my_organization', +]; + +$new_organization->setParameters($parameters); + +// If we're doing an update or patch call, we only need to provide the new fields +// Update will assume the required 'name' field stays the same, and will remove the +// value of any other field we don't provide. +// Patch will just update the title. +$parameters = [ + 'title' => 'New Title For This Organisation' +]; + +$existing_organization->setParameters($parameters); + +``` + +When you add parameters they will be checked to make sure they are valid for this object, an error will be thrown if +validation fails. + +Because CRUD-like objects can be used for multiple actions and this affects whether the ID is sent to the endpoint or +not, you need to set the action on the object: + +```php +setAction(CkanClient::CKAN_ACTION_CREATE); + +$existing_organization->setAction(CkanClient::CKAN_ACTION_PATCH); +``` + +Now you're ready to make the API call: + +```php +action($new_organization); + +$patch_result = $client->action($existing_organization); +``` + +In summary the code to create a brand new Organization in your CKAN repository could look like this: + +```php +setParameters(['name' => 'my_organisation']); +$new_organization->setAction(CkanClient::CKAN_ACTION_CREATE); +$client = \Drupal::service('ckan_connect.client'); +$create_result = $client->action($new_organization); +``` diff --git a/src/Ckan/CkanApiBase.php b/src/Ckan/CkanApiBase.php new file mode 100755 index 0000000..fe0f5ab --- /dev/null +++ b/src/Ckan/CkanApiBase.php @@ -0,0 +1,80 @@ + param_value]. + */ + protected $parameters = []; + + /** + * @var array $validParameters + * An array of valid parameter keys e.g. ['name', 'title', 'private', ...] + */ + protected $validParameters = []; + + /** + * @var array $requiredParameters + * An array of required parameter keys. + */ + protected $requiredParameters = []; + + /** + * {@inheritdoc} + */ + public function setParameters($parameters) { + if ($this->validateParameters($parameters)) { + $this->parameters = $parameters; + return TRUE; + } + // @todo: throw error instead. + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + return $this->machineName; + } + + /** + * {@inheritdoc} + */ + public function getParameters() { + return $this->parameters; + } + + /** + * Ensure the parameters are valid and complete. + * + * @param string $action + * The action being requested: create|update|patch. + */ + protected function validateParameters($parameters) { + $keys = array_keys($parameters); + + // Check for invalid parameter keys. + if (!empty(array_diff_key($keys, $this->validParameters))) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/src/Ckan/CkanApiInterface.php b/src/Ckan/CkanApiInterface.php new file mode 100755 index 0000000..1aabe31 --- /dev/null +++ b/src/Ckan/CkanApiInterface.php @@ -0,0 +1,38 @@ + param_value]. + * + * @return bool + */ + public function setParameters($parameters); + + /** + * Get the last part of the API endpoint specific to this CKAN object. + * + * @param string $action + * + * @return string + * The action slug, e.g. package_delete or organisation_create + */ + public function getPath(); + + /** + * Get the parameters to be used by the API call. + * + * @return array + * An associative array of parameters: [param_key => param_value]. + */ + public function getParameters(); + +} diff --git a/src/Ckan/CkanRequest.php b/src/Ckan/CkanRequest.php new file mode 100644 index 0000000..6ed24bf --- /dev/null +++ b/src/Ckan/CkanRequest.php @@ -0,0 +1,31 @@ +machineName = $action; + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function validateParameters($parameters) { + // We don't do any error checking for generic service calls. + return TRUE; + } + +} diff --git a/src/Ckan/Crud/CkanCrudBase.php b/src/Ckan/Crud/CkanCrudBase.php new file mode 100755 index 0000000..ef6ce3f --- /dev/null +++ b/src/Ckan/Crud/CkanCrudBase.php @@ -0,0 +1,134 @@ +id = $id; + } + + /** + * Set the action that the API client should perform with this object. + * + * @param string $action + * + * @return bool + */ + public function setAction($action) { + if (in_array($action, $this->validActions)) { + $this->action = $action; + return TRUE; + } + // @todo: throw error instead. + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + if (empty($this->action)){ + // @todo: throw error instead. + return FALSE; + } + return $this->machineName . '_' . $this->action; + } + + /** + * {@inheritdoc} + */ + public function getParameters() { + $actionsRequiringParameters = [ + CkanClient::CKAN_ACTION_CREATE, + CkanClient::CKAN_ACTION_UPDATE, + CkanClient::CKAN_ACTION_PATCH, + ]; + + if (in_array($this->action, $actionsRequiringParameters)) { + if ($this->prepareParameters()) { + return $this->parameters; + } + else { + // @todo: throw error instead. + return FALSE; + } + } + + // Delete and get (list/show) actions only require the ID. + return ['id' => $this->id]; + } + + /** + * {@inheritdoc} + */ + public function getValidActions() { + return $this->validActions; + } + + /** + * @param $action + * + * @return bool + */ + protected function prepareParameters() { + $keys = array_keys($this->parameters); + + // Check for missing required keys (create only). + if ($this->action === CkanClient::CKAN_ACTION_CREATE) { + if (!empty(array_diff_key($this->requiredParameters, $keys))) { + return FALSE; + } + } + + // Update and patch queries must include an ID. + if ($this->action === CkanClient::CKAN_ACTION_UPDATE || $this->action === CkanClient::CKAN_ACTION_PATCH) { + + // This check is the entire reason id is a separate property. + if (empty($this->id)) { + return FALSE; + } + + $this->parameters['id'] = $this->id; + } + return TRUE; + } + +} diff --git a/src/Ckan/Crud/CkanCrudInterface.php b/src/Ckan/Crud/CkanCrudInterface.php new file mode 100755 index 0000000..35375f5 --- /dev/null +++ b/src/Ckan/Crud/CkanCrudInterface.php @@ -0,0 +1,20 @@ +action === 'list') { + // This object doesn't follow the apparent naming convention in the CKAN + // API. + return $this->machineName . 's_' . $this->action; + } + return parent::getPath(); + } + +} diff --git a/src/Ckan/Crud/ResourceView.php b/src/Ckan/Crud/ResourceView.php new file mode 100755 index 0000000..a9950d9 --- /dev/null +++ b/src/Ckan/Crud/ResourceView.php @@ -0,0 +1,38 @@ +parameters)) { + $valid = FALSE; + } + + return $valid; + } + +} diff --git a/src/Ckan/Crud/Vocabulary.php b/src/Ckan/Crud/Vocabulary.php new file mode 100755 index 0000000..00fae76 --- /dev/null +++ b/src/Ckan/Crud/Vocabulary.php @@ -0,0 +1,33 @@ +getApiUrl() . '/' . $path; + $options = ['query' => $parameters]; + + if ($this->getApiKey()) { + $options['headers']['Authorization'] = $this->getApiKey(); + } + + $response = $this->httpClient->get($uri, $options) + ->getBody() + ->getContents(); + $response = json_decode($response); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function post($path, array $parameters) { $uri = $this->getApiUrl() . '/' . $path; - $options = ['query' => $query]; + $options = ['form_params' => $parameters]; if ($this->getApiKey()) { $options['headers']['Authorization'] = $this->getApiKey(); } - $response = $this->httpClient->get($uri, $options)->getBody()->getContents(); + $response = $this->httpClient->post($uri, $options) + ->getBody() + ->getContents(); $response = json_decode($response); return $response; } + /** + * Send an action query to the API. + * + * @param string $action + * @param \Drupal\ckan_connect\Ckan\CkanApiInterface $ckanObject + * + * @return mixed|\stdClass|string + */ + public function action(CkanApiInterface $ckanObject) { + $path = 'action/' . $ckanObject->getPath(); + $parameters = $ckanObject->getParameters(); + + // Every action API endpoint on CKAN may be used with a POST request. + return $this->post($path, $parameters); + } + } diff --git a/src/Client/CkanClientInterface.php b/src/Client/CkanClientInterface.php old mode 100644 new mode 100755 index f91255d..0dc1eb3 --- a/src/Client/CkanClientInterface.php +++ b/src/Client/CkanClientInterface.php @@ -46,16 +46,29 @@ public function getApiKey(); public function setApiKey($api_key); /** - * Get data from the CKAN endpoint. + * Get data from a CKAN endpoint. * * @param string $path * The path of the action. - * @param array $query + * @param array $parameters * The key pair parameters. * * @return \stdClass * A response object. */ - public function get($path, array $query = []); + public function get($path, array $parameters = []); + + /** + * Post data to a CKAN endpoint. + * + * @param string $path + * The path of the action. + * @param array $parameters + * The key pair parameters. + * + * @return \stdClass + * A response object. + */ + public function post($path, array $parameters); } diff --git a/src/Form/CkanConnectSettingsForm.php b/src/Form/CkanConnectSettingsForm.php old mode 100644 new mode 100755 diff --git a/src/Parser/CkanResourceUrlParser.php b/src/Parser/CkanResourceUrlParser.php old mode 100644 new mode 100755 diff --git a/src/Parser/CkanResourceUrlParserInterface.php b/src/Parser/CkanResourceUrlParserInterface.php old mode 100644 new mode 100755