From 564e9145a11fd5d02db50d1d8bc6e6c92513ac63 Mon Sep 17 00:00:00 2001 From: Xavier Lacot Date: Thu, 14 May 2020 17:29:50 +0200 Subject: [PATCH 1/2] Fixed endpoints names (Harvest documents several times the same endpoints, for different operations). Also, fixed some parameters definitions --- .gitignore | 3 + generated/harvest-openapi.yaml | 426 +++++++++++++++++++------------ src/Extractor/Extractor.php | 448 +++++++++++++++++++++++++++------ 3 files changed, 629 insertions(+), 248 deletions(-) diff --git a/.gitignore b/.gitignore index 2647471..25a013a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ composer.phar # CS .php_cs.cache +# Downloaded files +var + # Dependencies vendor diff --git a/generated/harvest-openapi.yaml b/generated/harvest-openapi.yaml index 6bf9fb2..b49ca8c 100644 --- a/generated/harvest-openapi.yaml +++ b/generated/harvest-openapi.yaml @@ -530,12 +530,12 @@ paths: schema: $ref: '#/definitions/Error' post: - summary: 'Mark an open invoice as a draft' - operationId: markOpenInvoiceAsDraft - description: 'Creates a new invoice message object and marks an open invoice as a draft. Returns an invoice message object and a 201 Created response code if the call succeeded.' + summary: 'Create an invoice message or change invoice status' + operationId: createInvoiceMessage + description: 'Creates a new invoice message object. Returns an invoice message object and a 201 Created response code if the call succeeded.' externalDocs: - description: 'Mark an open invoice as a draft' - url: 'https://help.getharvest.com/api-v2/invoices-api/invoices/invoice-messages/#mark-an-open-invoice-as-a-draft' + description: 'Create an invoice message' + url: 'https://help.getharvest.com/api-v2/invoices-api/invoices/invoice-messages/#create-an-invoice-message' security: - BearerAuth: [] @@ -556,12 +556,47 @@ paths: properties: event_type: type: string - description: 'Pass “draft” to mark the invoice as a draft.' + description: 'If provided, runs an event against the invoice. Options: close, draft, re-open, or send.' + recipients: + type: array + description: 'Array of recipient parameters. See below for details.' + items: + type: object + required: + - email + properties: + name: + description: 'Name of the message recipient.' + type: string + email: + description: 'Email of the message recipient.' + type: string + format: email + subject: + type: string + description: 'The message subject.' + body: + type: string + description: 'The message body.' + include_link_to_client_invoice: + type: boolean + description: 'If set to true, a link to the client invoice URL will be included in the message email. Defaults to false. Ignored when thank_you is set to true.' + attach_pdf: + type: boolean + description: 'If set to true, a PDF of the invoice will be attached to the message email. Defaults to false.' + send_me_a_copy: + type: boolean + description: 'If set to true, a copy of the message email will be sent to the current user. Defaults to false.' + thank_you: + type: boolean + description: 'If set to true, a thank you message email will be sent. Defaults to false.' required: - - event_type + - recipients responses: - 200: - description: 'Mark an open invoice as a draft' + 201: + description: 'Create an invoice message or change invoice status' + schema: + $ref: '#/definitions/InvoiceMessage' default: description: 'error payload' schema: @@ -793,12 +828,12 @@ paths: schema: $ref: '#/definitions/Error' post: - summary: 'Create an invoice based on tracked time and expenses' - operationId: createInvoiceBasedOnTrackedTimeAndExpenses + summary: 'Create an invoice' + operationId: createInvoice description: 'Creates a new invoice object. Returns an invoice object and a 201 Created response code if the call succeeded.' externalDocs: - description: 'Create an invoice based on tracked time and expenses' - url: 'https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/#create-an-invoice-based-on-tracked-time-and-expenses' + description: 'Create a free-form invoice' + url: 'https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/#create-a-free-form-invoice' security: - BearerAuth: [] @@ -865,11 +900,89 @@ paths: line_items_import: type: object description: 'An line items import object' + required: + - project_ids + properties: + project_ids: + description: 'An array of the client’s project IDs you’d like to include time/expenses from.' + type: array + items: + type: integer + time: + description: 'An time import object.' + type: object + required: + - summary_type + properties: + summary_type: + type: string + description: 'How to summarize the time entries per line item. Options: project, task, people, or detailed.' + from: + type: string + format: date + description: 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.' + to: + type: string + format: date + description: 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.' + expenses: + description: 'An expense import object.' + type: object + required: + - summary_type + properties: + summary_type: + type: string + description: 'How to summarize the expenses per line item. Options: project, category, people, or detailed.' + from: + type: string + format: date + description: 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.' + to: + type: string + format: date + description: 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.' + attach_receipt: + type: boolean + description: 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.' + line_items: + type: array + description: 'Array of line item parameters' + items: + type: object + required: + - kind + - unit_price + properties: + project_id: + description: 'The ID of the project associated with this line item.' + type: integer + format: int32 + kind: + description: 'The name of an invoice item category.' + type: string + description: + description: 'Text description of the line item.' + type: string + quantity: + description: 'The unit quantity of the item. Defaults to 1.' + type: number + format: float + unit_price: + description: 'The individual price per unit.' + type: number + format: float + taxed: + description: 'Whether the invoice’s tax percentage applies to this line item. Defaults to false.' + type: boolean + taxed2: + description: 'Whether the invoice’s tax2 percentage applies to this line item. Defaults to false.' + type: boolean required: - client_id responses: 201: - description: 'Create an invoice based on tracked time and expenses' + description: 'Create an invoice' schema: $ref: '#/definitions/Invoice' default: @@ -983,51 +1096,35 @@ paths: description: 'Array of line item parameters' items: type: object - required: - - project_ids properties: - project_ids: - description: 'An array of the client’s project IDs you’d like to include time/expenses from.' - type: array - items: - type: integer - time: - description: 'An time import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the time entries per line item. Options: project, task, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.' - to: - type: string - format: date - description: 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.' - expenses: - description: 'An expense import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the expenses per line item. Options: project, category, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.' - to: - type: string - format: date - description: 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.' - attach_receipt: - type: boolean - description: 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.' + id: + description: 'Unique ID for the line item.' + type: integer + format: int32 + project_id: + description: 'The ID of the project associated with this line item.' + type: integer + format: int32 + kind: + description: 'The name of an invoice item category.' + type: string + description: + description: 'Text description of the line item.' + type: string + quantity: + description: 'The unit quantity of the item. Defaults to 1.' + type: number + format: float + unit_price: + description: 'The individual price per unit.' + type: number + format: float + taxed: + description: 'Whether the invoice’s tax percentage applies to this line item. Defaults to false.' + type: boolean + taxed2: + description: 'Whether the invoice’s tax2 percentage applies to this line item. Defaults to false.' + type: boolean responses: 200: description: 'Update an invoice' @@ -1269,12 +1366,12 @@ paths: schema: $ref: '#/definitions/Error' post: - summary: 'Re-open a closed estimate' - operationId: reOpenClosedEstimate - description: 'Creates a new estimate message object and re-opens a closed estimate. Returns an estimate message object and a 201 Created response code if the call succeeded.' + summary: 'Create an estimate message or change estimate status' + operationId: createEstimateMessage + description: 'Creates a new estimate message object. Returns an estimate message object and a 201 Created response code if the call succeeded.' externalDocs: - description: 'Re-open a closed estimate' - url: 'https://help.getharvest.com/api-v2/estimates-api/estimates/estimate-messages/#re-open-a-closed-estimate' + description: 'Create an estimate message' + url: 'https://help.getharvest.com/api-v2/estimates-api/estimates/estimate-messages/#create-an-estimate-message' security: - BearerAuth: [] @@ -1295,12 +1392,38 @@ paths: properties: event_type: type: string - description: 'Pass “re-open” to re-open the estimate.' + description: 'If provided, runs an event against the estimate. Options: “accept”, “decline”, “re-open”, or “send”.' + recipients: + type: array + description: 'Array of recipient parameters. See below for details.' + items: + type: object + required: + - email + properties: + name: + description: 'Name of the message recipient.' + type: string + email: + description: 'Email of the message recipient.' + type: string + format: email + subject: + type: string + description: 'The message subject.' + body: + type: string + description: 'The message body.' + send_me_a_copy: + type: boolean + description: 'If set to true, a copy of the message email will be sent to the current user. Defaults to false.' required: - - event_type + - recipients responses: - 200: - description: 'Re-open a closed estimate' + 201: + description: 'Create an estimate message or change estimate status' + schema: + $ref: '#/definitions/EstimateMessage' default: description: 'error payload' schema: @@ -1460,50 +1583,29 @@ paths: items: type: object required: - - project_ids + - kind + - unit_price properties: - project_ids: - description: 'An array of the client’s project IDs you’d like to include time/expenses from.' - type: array - items: - type: integer - time: - description: 'An time import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the time entries per line item. Options: project, task, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.' - to: - type: string - format: date - description: 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.' - expenses: - description: 'An expense import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the expenses per line item. Options: project, category, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.' - to: - type: string - format: date - description: 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.' - attach_receipt: - type: boolean - description: 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.' + kind: + description: 'The name of an estimate item category.' + type: string + description: + description: 'Text description of the line item.' + type: string + quantity: + description: 'The unit quantity of the item. Defaults to 1.' + type: integer + format: int32 + unit_price: + description: 'The individual price per unit.' + type: number + format: float + taxed: + description: 'Whether the estimate’s tax percentage applies to this line item. Defaults to false.' + type: boolean + taxed2: + description: 'Whether the estimate’s tax2 percentage applies to this line item. Defaults to false.' + type: boolean required: - client_id responses: @@ -1607,51 +1709,31 @@ paths: description: 'Array of line item parameters' items: type: object - required: - - project_ids properties: - project_ids: - description: 'An array of the client’s project IDs you’d like to include time/expenses from.' - type: array - items: - type: integer - time: - description: 'An time import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the time entries per line item. Options: project, task, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.' - to: - type: string - format: date - description: 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.' - expenses: - description: 'An expense import object.' - type: object - required: - - summary_type - properties: - summary_type: - type: string - description: 'How to summarize the expenses per line item. Options: project, category, people, or detailed.' - from: - type: string - format: date - description: 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.' - to: - type: string - format: date - description: 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.' - attach_receipt: - type: boolean - description: 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.' + id: + description: 'Unique ID for the line item.' + type: integer + format: int32 + kind: + description: 'The name of an estimate item category.' + type: string + description: + description: 'Text description of the line item.' + type: string + quantity: + description: 'The unit quantity of the item. Defaults to 1.' + type: integer + format: int32 + unit_price: + description: 'The individual price per unit.' + type: number + format: float + taxed: + description: 'Whether the estimate’s tax percentage applies to this line item. Defaults to false.' + type: boolean + taxed2: + description: 'Whether the estimate’s tax2 percentage applies to this line item. Defaults to false.' + type: boolean responses: 200: description: 'Update an estimate' @@ -2525,6 +2607,12 @@ paths: required: false in: query type: integer + - + name: external_reference_id + description: 'Only return time entries with the given external_reference ID.' + required: false + in: query + type: string - name: is_billed description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.' @@ -2577,12 +2665,12 @@ paths: schema: $ref: '#/definitions/Error' post: - summary: 'Create a time entry via start and end time' - operationId: createTimeEntryViaStartAndEndTime - description: "Creates a new time entry object. Returns a time entry object and a 201 Created response code if the call succeeded.\n\nYou should only use this method to create time entries when your account is configured to track time via start and end time. You can verify this by visiting the Settings page in your Harvest account or by checking if wants_timestamp_timers is true in the Company API." + summary: 'Create a time entry' + operationId: createTimeEntry + description: "Creates a new time entry object. Returns a time entry object and a 201 Created response code if the call succeeded.\n\nYou should only use this method to create time entries when your account is configured to track time via duration. You can verify this by visiting the Settings page in your Harvest account or by checking if wants_timestamp_timers is false in the Company API." externalDocs: - description: 'Create a time entry via start and end time' - url: 'https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#create-a-time-entry-via-start-and-end-time' + description: 'Create a time entry via duration' + url: 'https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#create-a-time-entry-via-duration' security: - BearerAuth: [] @@ -2631,13 +2719,17 @@ paths: type: string permalink: type: string + hours: + type: number + description: 'The current amount of time tracked. If provided, the time entry will be created with the specified hours and is_running will be set to false. If not provided, hours will be set to 0.0 and is_running will be set to true.' + format: float required: - project_id - task_id - spent_date responses: 201: - description: 'Create a time entry via start and end time' + description: 'Create a time entry' schema: $ref: '#/definitions/TimeEntry' default: @@ -4221,7 +4313,7 @@ paths: post: summary: 'Create a user' operationId: createUser - description: 'Creates a new user object. Returns a user object and a 201 Created response code if the call succeeded.' + description: 'Creates a new user object and sends an invitation email to the address specified in the email parameter. Returns a user object and a 201 Created response code if the call succeeded.' externalDocs: description: 'Create a user' url: 'https://help.getharvest.com/api-v2/users-api/users/users/#create-a-user' @@ -4469,7 +4561,7 @@ paths: /reports/expenses/clients: get: summary: 'Clients Report' - operationId: clientsReport + operationId: clientsExpensesReport description: '' externalDocs: description: 'Clients Report' @@ -4515,7 +4607,7 @@ paths: /reports/expenses/projects: get: summary: 'Projects Report' - operationId: projectsReport + operationId: projectsExpensesReport description: '' externalDocs: description: 'Projects Report' @@ -4607,7 +4699,7 @@ paths: /reports/expenses/team: get: summary: 'Team Report' - operationId: teamReport + operationId: teamExpensesReport description: '' externalDocs: description: 'Team Report' @@ -4699,7 +4791,7 @@ paths: /reports/time/clients: get: summary: 'Clients Report' - operationId: clientsReport + operationId: clientsTimeReport description: '' externalDocs: description: 'Clients Report' @@ -4745,7 +4837,7 @@ paths: /reports/time/projects: get: summary: 'Projects Report' - operationId: projectsReport + operationId: projectsTimeReport description: '' externalDocs: description: 'Projects Report' @@ -4837,7 +4929,7 @@ paths: /reports/time/team: get: summary: 'Team Report' - operationId: teamReport + operationId: teamTimeReport description: '' externalDocs: description: 'Team Report' diff --git a/src/Extractor/Extractor.php b/src/Extractor/Extractor.php index 2c56da4..0aa1033 100644 --- a/src/Extractor/Extractor.php +++ b/src/Extractor/Extractor.php @@ -35,12 +35,32 @@ public function extract() $this->buildPluralDefinitions(); $this->buildItemsTypes(); + $this->printUnknownDefinitions($this->paths); + return [ 'definitions' => $this->definitions, 'paths' => $this->paths, ]; } + private function printUnknownDefinitions(array $items) + { + foreach ($items as $key => $item) { + if (is_array($item)) { + $this->printUnknownDefinitions($item); + } elseif ('$ref' === $key) { + $item = substr($item, 14); + + if (!isset($this->definitions[$item]) && 'Error' !== $item) { + throw new \LogicException(sprintf( + 'Unknown definition: %s', + $item + )); + } + } + } + } + public static function buildDefinitionProperties($propertyTexts) { $properties = []; @@ -60,12 +80,17 @@ public static function buildDefinitionProperties($propertyTexts) return $properties; } - public static function buildDefinitionProperty($name, $type, $description) + public static function buildDefinitionProperty($name, $type, $description, $path = null, $method = null) { $arrayof = null; $fixedType = self::convertType($type); $format = self::detectFormat($name, $type); + $property = [ + 'type' => $fixedType, + 'description' => $description, + ]; + if ('array' === $type) { if (preg_match('/^Array of (.+)$/', $description, $matches)) { $arrayof = self::singularize(self::camelize($matches[1])); @@ -84,87 +109,260 @@ public static function buildDefinitionProperty($name, $type, $description) $arrayof = 'string'; } - $property = [ - 'type' => $fixedType, - 'description' => $description, - ]; + if (null !== $arrayof) { + $property['arrayof'] = $arrayof; + } - if ('line_items' === $name) { + if ('Array of recipient parameters. See below for details.' === $description) { + unset($property['arrayof']); $property['items'] = [ 'type' => 'object', 'required' => [ - 'project_ids', + 'email', ], 'properties' => [ - 'project_ids' => [ - 'description' => 'An array of the client’s project IDs you’d like to include time/expenses from.', - 'type' => 'array', - 'items' => [ - 'type' => 'integer', - ], + 'name' => [ + 'description' => 'Name of the message recipient.', + 'type' => 'string', + ], + 'email' => [ + 'description' => 'Email of the message recipient.', + 'type' => 'string', + 'format' => 'email', + ], + ], + ]; + } elseif ('line_items_import' === $name) { + $property['required'] = [ + 'project_ids', + ]; + $property['properties'] = [ + 'project_ids' => [ + 'description' => 'An array of the client’s project IDs you’d like to include time/expenses from.', + 'type' => 'array', + 'items' => [ + 'type' => 'integer', + ], + ], + 'time' => [ + 'description' => 'An time import object.', + 'type' => 'object', + 'required' => [ + 'summary_type', ], - 'time' => [ - 'description' => 'An time import object.', - 'type' => 'object', - 'required' => [ - 'summary_type', - ], - 'properties' => [ - 'summary_type' => [ - 'type' => 'string', - 'description' => 'How to summarize the time entries per line item. Options: project, task, people, or detailed.', - ], - 'from' => [ - 'type' => 'string', - 'format' => 'date', - 'description' => 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.', - ], - 'to' => [ - 'type' => 'string', - 'format' => 'date', - 'description' => 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.', - ], + 'properties' => [ + 'summary_type' => [ + 'type' => 'string', + 'description' => 'How to summarize the time entries per line item. Options: project, task, people, or detailed.', ], + 'from' => [ + 'type' => 'string', + 'format' => 'date', + 'description' => 'Start date for included time entries. Must be provided if to is present. If neither from or to are provided, all unbilled time entries will be included.', + ], + 'to' => [ + 'type' => 'string', + 'format' => 'date', + 'description' => 'End date for included time entries. Must be provided if from is present. If neither from or to are provided, all unbilled time entries will be included.', + ], + ], + ], + 'expenses' => [ + 'description' => 'An expense import object.', + 'type' => 'object', + 'required' => [ + 'summary_type', ], - 'expenses' => [ - 'description' => 'An expense import object.', - 'type' => 'object', - 'required' => [ - 'summary_type', - ], - 'properties' => [ - 'summary_type' => [ - 'type' => 'string', - 'description' => 'How to summarize the expenses per line item. Options: project, category, people, or detailed.', - ], - 'from' => [ - 'type' => 'string', - 'format' => 'date', - 'description' => 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.', - ], - 'to' => [ - 'type' => 'string', - 'format' => 'date', - 'description' => 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.', - ], - 'attach_receipt' => [ - 'type' => 'boolean', - 'description' => 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.', - ], + 'properties' => [ + 'summary_type' => [ + 'type' => 'string', + 'description' => 'How to summarize the expenses per line item. Options: project, category, people, or detailed.', + ], + 'from' => [ + 'type' => 'string', + 'format' => 'date', + 'description' => 'Start date for included expenses. Must be provided if to is present. If neither from or to are provided, all unbilled expenses will be included.', + ], + 'to' => [ + 'type' => 'string', + 'format' => 'date', + 'description' => 'End date for included expenses. Must be provided if from is present. If neither from or to are provided, all unbilled expenses will be included.', + ], + 'attach_receipt' => [ + 'type' => 'boolean', + 'description' => 'If set to true, a PDF containing an expense report with receipts will be attached to the invoice. Defaults to false.', ], ], ], ]; + } elseif ('line_items' === $name) { + if ('/invoices' === $path) { + unset($property['arrayof']); + $property['items'] = [ + 'type' => 'object', + 'required' => [ + 'kind', + 'unit_price', + ], + 'properties' => [ + 'project_id' => [ + 'description' => 'The ID of the project associated with this line item.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'kind' => [ + 'description' => 'The name of an invoice item category.', + 'type' => 'string', + ], + 'description' => [ + 'description' => 'Text description of the line item.', + 'type' => 'string', + ], + 'quantity' => [ + 'description' => 'The unit quantity of the item. Defaults to 1.', + 'type' => 'number', + 'format' => 'float', + ], + 'unit_price' => [ + 'description' => 'The individual price per unit.', + 'type' => 'number', + 'format' => 'float', + ], + 'taxed' => [ + 'description' => 'Whether the invoice’s tax percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + 'taxed2' => [ + 'description' => 'Whether the invoice’s tax2 percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + ], + ]; + } elseif ('/invoices/{invoiceId}' === $path) { + unset($property['arrayof']); + $property['items'] = [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => 'Unique ID for the line item.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'project_id' => [ + 'description' => 'The ID of the project associated with this line item.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'kind' => [ + 'description' => 'The name of an invoice item category.', + 'type' => 'string', + ], + 'description' => [ + 'description' => 'Text description of the line item.', + 'type' => 'string', + ], + 'quantity' => [ + 'description' => 'The unit quantity of the item. Defaults to 1.', + 'type' => 'number', + 'format' => 'float', + ], + 'unit_price' => [ + 'description' => 'The individual price per unit.', + 'type' => 'number', + 'format' => 'float', + ], + 'taxed' => [ + 'description' => 'Whether the invoice’s tax percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + 'taxed2' => [ + 'description' => 'Whether the invoice’s tax2 percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + ], + ]; + } elseif ('/estimates' === $path) { + unset($property['arrayof']); + $property['items'] = [ + 'type' => 'object', + 'required' => [ + 'kind', + 'unit_price', + ], + 'properties' => [ + 'kind' => [ + 'description' => 'The name of an estimate item category.', + 'type' => 'string', + ], + 'description' => [ + 'description' => 'Text description of the line item.', + 'type' => 'string', + ], + 'quantity' => [ + 'description' => 'The unit quantity of the item. Defaults to 1.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'unit_price' => [ + 'description' => 'The individual price per unit.', + 'type' => 'number', + 'format' => 'float', + ], + 'taxed' => [ + 'description' => 'Whether the estimate’s tax percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + 'taxed2' => [ + 'description' => 'Whether the estimate’s tax2 percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + ], + ]; + } elseif ('/estimates/{estimateId}' === $path) { + unset($property['arrayof']); + $property['items'] = [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => 'Unique ID for the line item.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'kind' => [ + 'description' => 'The name of an estimate item category.', + 'type' => 'string', + ], + 'description' => [ + 'description' => 'Text description of the line item.', + 'type' => 'string', + ], + 'quantity' => [ + 'description' => 'The unit quantity of the item. Defaults to 1.', + 'type' => 'integer', + 'format' => 'int32', + ], + 'unit_price' => [ + 'description' => 'The individual price per unit.', + 'type' => 'number', + 'format' => 'float', + ], + 'taxed' => [ + 'description' => 'Whether the estimate’s tax percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + 'taxed2' => [ + 'description' => 'Whether the estimate’s tax2 percentage applies to this line item. Defaults to false.', + 'type' => 'boolean', + ], + ], + ]; + } } if (null !== $format) { $property['format'] = $format; } - if (null !== $arrayof) { - $property['arrayof'] = $arrayof; - } - if ('object' === $type) { $desc = str_replace(',,', ',', str_replace(' and ', ', ', $description)); $desc = str_replace('has been invoiced, this field', 'has been invoiced this field', $desc); @@ -184,7 +382,7 @@ public static function buildDefinitionProperty($name, $type, $description) $property['properties'] = [$matches[1] => [ 'type' => self::guessFieldType($matches[1]), ]]; - } else { + } elseif (!isset($property['properties'])) { echo "$name\t$desc\n"; } } @@ -231,8 +429,10 @@ public function buildPath($url, $path, $method, $node, $title) } return [ - 'summary' => $summary, - 'operationId' => self::buildOperationId($method, $summary), + 'summary' => self::cleanupSummary($summary), + 'operationId' => self::cleanupOperationId( + self::buildOperationId($path, $method, $summary) + ), 'description' => implode("\n\n", array_reverse($description)), 'externalDocs' => [ 'description' => $summary, @@ -244,12 +444,12 @@ public function buildPath($url, $path, $method, $node, $title) 'AccountAuth' => [], ], ], - 'parameters' => self::buildPathParameters($method, $pathParameters, $explicitParameters, $explicitParametersColumns), + 'parameters' => self::buildPathParameters($method, $path, $pathParameters, $explicitParameters, $explicitParametersColumns), 'responses' => self::buildPathResponse($method, $summary, $title), ]; } - public static function buildPathParameters($method, $pathParameters, $explicitParameters, $explicitParametersColumns) + public static function buildPathParameters($method, $path, $pathParameters, $explicitParameters, $explicitParametersColumns) { $parameters = []; @@ -259,7 +459,7 @@ public static function buildPathParameters($method, $pathParameters, $explicitPa if (\count($explicitParameters) > 0) { if (\in_array($method, ['patch', 'post'], true)) { - $parameters[] = self::buildPathBodyParameter($explicitParameters, $explicitParametersColumns); + $parameters[] = self::buildPathBodyParameter($method, $path, $explicitParameters, $explicitParametersColumns); } else { while (\count($explicitParameters) > 0) { $required = false; @@ -276,7 +476,7 @@ public static function buildPathParameters($method, $pathParameters, $explicitPa return $parameters; } - public static function buildPathBodyParameter($explicitParameters, $explicitParametersColumns) + public static function buildPathBodyParameter($method, $path, $explicitParameters, $explicitParametersColumns) { $requiredProperties = []; $properties = []; @@ -294,7 +494,7 @@ public static function buildPathBodyParameter($explicitParameters, $explicitPara $requiredProperties[] = $parameter; } - $property = self::buildDefinitionProperty($parameter, $type, $description); + $property = self::buildDefinitionProperty($parameter, $type, $description, $path, $method); $properties[$parameter] = $property; } @@ -337,8 +537,22 @@ public static function buildPathQueryParameter($name, $type, $description, $requ ]; } - public static function buildOperationId($method, $summary) + public static function buildOperationId($path, $method, $summary) { + if ('/reports/time/clients' === $path) { + return 'clientsTimeReport'; + } elseif ('/reports/expenses/clients' === $path) { + return 'clientsExpensesReport'; + } elseif ('/reports/time/team' === $path) { + return 'teamTimeReport'; + } elseif ('/reports/expenses/team' === $path) { + return 'teamExpensesReport'; + } elseif ('/reports/time/projects' === $path) { + return 'projectsTimeReport'; + } elseif ('/reports/expenses/projects' === $path) { + return 'projectsExpensesReport'; + } + $summary = str_replace([' all ', ' an ', ' a '], ' ', $summary); $summary = str_replace('’s', '', $summary); @@ -348,7 +562,7 @@ public static function buildOperationId($method, $summary) public static function buildPathResponse($method, $summary, $title) { $successResponse = [ - 'description' => $summary, + 'description' => self::cleanupSummary($summary), ]; $successResponseSchema = self::guessPathResponseSchema($summary, $title); @@ -376,6 +590,27 @@ public static function camelize($word) return trim(str_replace($separators, '', ucwords(strtolower($word), implode('', $separators))), " \t."); } + private static function cleanupOperationId($operationId) + { + $conversionMap = [ + 'createFreeFormInvoice' => 'createInvoice', + 'createTimeEntryViaDuration' => 'createTimeEntry', + ]; + + return isset($conversionMap[$operationId]) ? $conversionMap[$operationId] : $operationId; + } + + private static function cleanupSummary($summary) { + $summaries = [ + 'Create an invoice message' => 'Create an invoice message or change invoice status', + 'Create a free-form invoice' => 'Create an invoice', + 'Create an estimate message' => 'Create an estimate message or change estimate status', + 'Create a time entry via duration' => 'Create a time entry', + ]; + + return isset($summaries[$summary]) ? $summaries[$summary] : $summary; + } + public static function convertType($type) { $conversionMap = [ @@ -483,6 +718,14 @@ public static function guessPathResponseSchema($summary, $title) $result = $guesser($summary); + if ('#/definitions/Free' === $result) { + $result = '#/definitions/Invoice'; + } + + if ('#/definitions/TimeEntryViaDuration' === $result) { + $result = '#/definitions/TimeEntry'; + } + if ('#/definitions/TimeEntryViaStartAndEndTime' === $result) { $result = '#/definitions/TimeEntry'; } @@ -556,10 +799,10 @@ private function buildItemsTypes() if (isset($property['arrayof'])) { if (isset($this->definitions[$property['arrayof']])) { $this->definitions[$definitionName]['properties'][$propertyName]['items'] = ['$ref' => '#/definitions/'.$property['arrayof']]; - } - - if (\in_array($property['arrayof'], self::BASE_TYPES, true)) { + } elseif (\in_array($property['arrayof'], self::BASE_TYPES, true)) { $this->definitions[$definitionName]['properties'][$propertyName]['items'] = ['type' => $property['arrayof']]; + } else { + echo $property['arrayof'] . "\n"; } unset($this->definitions[$definitionName]['properties'][$propertyName]['arrayof']); @@ -583,10 +826,15 @@ private function buildItemsTypes() if (isset($property['arrayof'])) { if (isset($this->definitions[$property['arrayof']])) { $this->paths[$pathName][$methodName]['parameters'][$id]['schema']['properties'][$propertyName]['items'] = ['$ref' => '#/definitions/'.$property['arrayof']]; - } - - if (\in_array($property['arrayof'], self::BASE_TYPES, true)) { + } elseif (\in_array($property['arrayof'], self::BASE_TYPES, true)) { $this->paths[$pathName][$methodName]['parameters'][$id]['schema']['properties'][$propertyName]['items'] = ['type' => $property['arrayof']]; + } else { + echo sprintf( + "%s %s - %s\n", + $methodName, + $pathName, + $property['arrayof'] + ); } unset($this->paths[$pathName][$methodName]['parameters'][$id]['schema']['properties'][$propertyName]['arrayof']); @@ -661,9 +909,26 @@ private function buildPluralDefinitions() } } + private function download($url): string + { + $key = md5($url); + $cacheDirectory = __DIR__ . '/../../var/download/'; + $path = $cacheDirectory . $key . '.txt'; + + if (!is_dir($cacheDirectory)) { + mkdir($cacheDirectory, 0700, true); + } + + if (!file_exists($path)) { + copy($url, $path); + } + + return file_get_contents($path); + } + private function extractApiDoc($url) { - $crawler = new Crawler(file_get_contents($url)); + $crawler = new Crawler($this->download($url)); $title = trim($crawler->filter('h1')->text()); @@ -705,7 +970,28 @@ private function extractApiDoc($url) $this->paths[$path] = []; } - $this->paths[$path][$method] = self::buildPath($url, $path, $method, $node, $title); + $operation = self::buildPath($url, $path, $method, $node, $title); + + if (!isset($this->paths[$path][$method])) { + $this->paths[$path][$method] = $operation; + } else { + // add possible additionnal body parameters + $bodyParams = array_filter($operation['parameters'], function($item) { + return $item['in'] === 'body'; + }); + if (count($bodyParams) > 0) { + foreach ($this->paths[$path][$method]['parameters'] as $key => $parameter) { + if ($parameter['in'] === 'body') { + $this->paths[$path][$method]['parameters'][$key]['schema']['properties'] = array_merge( + array_shift($bodyParams)['schema']['properties'], + $this->paths[$path][$method]['parameters'][$key]['schema']['properties'] + ); + + return; + } + } + } + } } }); } From 56582810270612f598d181ee7a3638ced0c8a0f6 Mon Sep 17 00:00:00 2001 From: Xavier Lacot Date: Thu, 14 May 2020 17:31:08 +0200 Subject: [PATCH 2/2] CS fixes --- src/Extractor/Extractor.php | 90 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/src/Extractor/Extractor.php b/src/Extractor/Extractor.php index 0aa1033..ecb5870 100644 --- a/src/Extractor/Extractor.php +++ b/src/Extractor/Extractor.php @@ -43,24 +43,6 @@ public function extract() ]; } - private function printUnknownDefinitions(array $items) - { - foreach ($items as $key => $item) { - if (is_array($item)) { - $this->printUnknownDefinitions($item); - } elseif ('$ref' === $key) { - $item = substr($item, 14); - - if (!isset($this->definitions[$item]) && 'Error' !== $item) { - throw new \LogicException(sprintf( - 'Unknown definition: %s', - $item - )); - } - } - } - } - public static function buildDefinitionProperties($propertyTexts) { $properties = []; @@ -590,27 +572,6 @@ public static function camelize($word) return trim(str_replace($separators, '', ucwords(strtolower($word), implode('', $separators))), " \t."); } - private static function cleanupOperationId($operationId) - { - $conversionMap = [ - 'createFreeFormInvoice' => 'createInvoice', - 'createTimeEntryViaDuration' => 'createTimeEntry', - ]; - - return isset($conversionMap[$operationId]) ? $conversionMap[$operationId] : $operationId; - } - - private static function cleanupSummary($summary) { - $summaries = [ - 'Create an invoice message' => 'Create an invoice message or change invoice status', - 'Create a free-form invoice' => 'Create an invoice', - 'Create an estimate message' => 'Create an estimate message or change estimate status', - 'Create a time entry via duration' => 'Create a time entry', - ]; - - return isset($summaries[$summary]) ? $summaries[$summary] : $summary; - } - public static function convertType($type) { $conversionMap = [ @@ -792,6 +753,43 @@ public static function endsWith($haystack, $needle) return substr($haystack, -$length) === $needle; } + private function printUnknownDefinitions(array $items) + { + foreach ($items as $key => $item) { + if (\is_array($item)) { + $this->printUnknownDefinitions($item); + } elseif ('$ref' === $key) { + $item = substr($item, 14); + + if (!isset($this->definitions[$item]) && 'Error' !== $item) { + throw new \LogicException(sprintf('Unknown definition: %s', $item)); + } + } + } + } + + private static function cleanupOperationId($operationId) + { + $conversionMap = [ + 'createFreeFormInvoice' => 'createInvoice', + 'createTimeEntryViaDuration' => 'createTimeEntry', + ]; + + return isset($conversionMap[$operationId]) ? $conversionMap[$operationId] : $operationId; + } + + private static function cleanupSummary($summary) + { + $summaries = [ + 'Create an invoice message' => 'Create an invoice message or change invoice status', + 'Create a free-form invoice' => 'Create an invoice', + 'Create an estimate message' => 'Create an estimate message or change estimate status', + 'Create a time entry via duration' => 'Create a time entry', + ]; + + return isset($summaries[$summary]) ? $summaries[$summary] : $summary; + } + private function buildItemsTypes() { foreach ($this->definitions as $definitionName => $definition) { @@ -802,7 +800,7 @@ private function buildItemsTypes() } elseif (\in_array($property['arrayof'], self::BASE_TYPES, true)) { $this->definitions[$definitionName]['properties'][$propertyName]['items'] = ['type' => $property['arrayof']]; } else { - echo $property['arrayof'] . "\n"; + echo $property['arrayof']."\n"; } unset($this->definitions[$definitionName]['properties'][$propertyName]['arrayof']); @@ -912,8 +910,8 @@ private function buildPluralDefinitions() private function download($url): string { $key = md5($url); - $cacheDirectory = __DIR__ . '/../../var/download/'; - $path = $cacheDirectory . $key . '.txt'; + $cacheDirectory = __DIR__.'/../../var/download/'; + $path = $cacheDirectory.$key.'.txt'; if (!is_dir($cacheDirectory)) { mkdir($cacheDirectory, 0700, true); @@ -976,12 +974,12 @@ private function extractApiDoc($url) $this->paths[$path][$method] = $operation; } else { // add possible additionnal body parameters - $bodyParams = array_filter($operation['parameters'], function($item) { - return $item['in'] === 'body'; + $bodyParams = array_filter($operation['parameters'], function ($item) { + return 'body' === $item['in']; }); - if (count($bodyParams) > 0) { + if (\count($bodyParams) > 0) { foreach ($this->paths[$path][$method]['parameters'] as $key => $parameter) { - if ($parameter['in'] === 'body') { + if ('body' === $parameter['in']) { $this->paths[$path][$method]['parameters'][$key]['schema']['properties'] = array_merge( array_shift($bodyParams)['schema']['properties'], $this->paths[$path][$method]['parameters'][$key]['schema']['properties']