Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/preview.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Preview generated SDK vs main

on:
pull_request:
paths:
- "sdk-generator/**"
- "package-lock.json"
workflow_dispatch:

jobs:
generate-and-diff:
name: Generate SDK and diff vs main
runs-on: ubuntu-latest

steps:
- name: Checkout current ref
uses: actions/checkout@v4
with:
token: ${{ secrets.MACHINE_USER_PAT }}

- name: Checkout main
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: main
path: main-sdk
token: ${{ secrets.MACHINE_USER_PAT }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: |
package-lock.json

- name: Login to GitHub Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.MACHINE_USER_PAT }}

- name: Install dependencies
run: npm ci

- name: Build PHP SDK
run: |
mkdir -p /tmp/sdk
npm run generate /tmp/sdk https://www.rebilly.com/_spec/catalog/all.yaml
cp -r examples /tmp/sdk

- name: Generate diff (generated SDK vs main)
id: text-diff
working-directory: main-sdk
run: |
# Replace main's SDK files with generated ones, then diff
rm -rf src composer.json .php-cs-fixer.php psalm.xml 2>/dev/null || true
cp -r /tmp/sdk/src .
cp /tmp/sdk/composer.json . 2>/dev/null || true
cp /tmp/sdk/psalm.xml . 2>/dev/null || true
cp /tmp/sdk/.php-cs-fixer.php . 2>/dev/null || true

# Add all files to git to ensure diff works correctly
git add -A || true

# Generate text diff (staged = generated vs main's content)
git diff --no-color --cached > /tmp/rebilly-sdk-diff.txt || true

# Check diff size (GitHub PR comment limit is ~65,536 characters)
DIFF_SIZE=$(wc -c < /tmp/rebilly-sdk-diff.txt)
echo "diff_size=$DIFF_SIZE" >> $GITHUB_OUTPUT
echo "Diff size: $DIFF_SIZE bytes"

if [ "$DIFF_SIZE" -gt 0 ] && [ "$DIFF_SIZE" -lt 60000 ]; then
echo "include_diff=true" >> $GITHUB_OUTPUT
else
echo "include_diff=false" >> $GITHUB_OUTPUT
fi
if [ "$DIFF_SIZE" -eq 0 ]; then
echo "diff_empty=true" >> $GITHUB_OUTPUT
else
echo "diff_empty=false" >> $GITHUB_OUTPUT
fi

- name: Generate HTML diff
working-directory: main-sdk
run: |
npx -y diff2html-cli -s side -t 'Rebilly PHP SDK' -F /tmp/rebilly-sdk-diff.html

- name: Upload Artifact
id: diff-upload
uses: actions/upload-artifact@v4
with:
name: rebilly-sdk-diff
path: /tmp/rebilly-sdk-diff.html
retention-days: 3
if-no-files-found: ignore


- name: Prepare PR Comment Message
if: github.event_name == 'pull_request'
id: comment-message
run: |
echo "## 🔍 PHP SDK Changes Preview" >> /tmp/pr-comment.md
echo "" >> /tmp/pr-comment.md
if [ "${{ steps.text-diff.outputs.include_diff }}" == "true" ]; then
{
echo '```diff'
cat /tmp/rebilly-sdk-diff.txt
echo '```'
} >> /tmp/pr-comment.md
else
echo "**Diff is too large to display inline**" >> /tmp/pr-comment.md
fi
echo "" >> /tmp/pr-comment.md
echo "📦 Download the **rebilly-sdk-diff** artifact from the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for a formatted HTML diff." >> /tmp/pr-comment.md
echo "" >> /tmp/pr-comment.md
echo "---" >> /tmp/pr-comment.md
echo "**Updated:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> /tmp/pr-comment.md
echo "**Commit:** [${{ github.event.pull_request.head.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }})" >> /tmp/pr-comment.md

{
echo "message<<EOF"
cat /tmp/pr-comment.md
echo "EOF"
} >> $GITHUB_OUTPUT

- name: Create/Update PR Comment
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
GITHUB_TOKEN: ${{ github.token }}
number: ${{ github.event.pull_request.number }}
message: ${{ steps.comment-message.outputs.message }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
},
"scripts": {
"version": "npx changeset version && npm i && node scripts/update-sdk-version.js",
"generate": "regenerator generate -g rebilly-php -o"
"generate": "regenerator generate -g rebilly-php -c sdk-generator/rebilly-php.config.ts -o"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const config: GeneratorConfig<PHPCodeGen, AnyStaticContext> = {
},

buildConfig: {
outputFilename: 'rebilly-php',
outputFilename: 'templates',
},

rootNameSpace: 'Rebilly\\Sdk',
Expand Down Expand Up @@ -287,7 +287,7 @@ const config: GeneratorConfig<PHPCodeGen, AnyStaticContext> = {
'operation-pdf': 'operation-pdf.php.handlebars',
},

templateDirs: ['@bundled/rebilly-php'],
templateDirs: ['./sdk-generator/templates', '@bundled/php'],
};

export default config;
10 changes: 10 additions & 0 deletions sdk-generator/templates/file-header.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* This source file is proprietary and part of Rebilly.
*
* (c) Rebilly SRL
* Rebilly Ltd.
* Rebilly Inc.
*
* @see https://www.rebilly.com
*/

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
{{>file-header}}
declare(strict_types=1);

namespace {{fqn model.namespace}};

class {{model.className}}Factory
{
public static function from(array $data = [], array $metadata = []): {{model.className}}
{
if (count($data) === 1 && isset($data['id'])) {
return OriginalPlan::from($data, $metadata);
}

return FlexiblePlanFactory::from($data, $metadata);
}
}
20 changes: 20 additions & 0 deletions sdk-generator/templates/model-factory-Plan.php.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
{{>file-header}}
declare(strict_types=1);

namespace {{fqn model.namespace}};

class {{model.className}}Factory
{
public static function from(array $data = [], array $metadata = []): {{model.className}}
{
if ($data['isTrialOnly'] ?? false) {
return TrialOnlyPlan::from($data, $metadata);
}
if (isset($data['recurringInterval'])) {
return SubscriptionPlan::from($data, $metadata);
}

return OneTimeSalePlan::from($data, $metadata);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#>operation-layout}}
$response = $this->client->send($request);
$data = Utils::jsonDecode((string) $response->getBody(), true);

{{#with responseType.[0].containedTypes.[0]}}
return new Collection(
array_map(fn (array $item): {{classname name}} => {{classname name}}{{#if (isFactory name)}}Factory{{/if}}::from($item, ['headers' => $response->getHeaders()]), $data),
(int) $response->getHeaderLine(Collection::HEADER_LIMIT),
(int) $response->getHeaderLine(Collection::HEADER_OFFSET),
(int) $response->getHeaderLine(Collection::HEADER_TOTAL),
[
'headers' => $response->getHeaders(),
]
);
{{/with}}
{{/operation-layout}}
17 changes: 17 additions & 0 deletions sdk-generator/templates/operation-paginator.php.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{#if returnTypeHint}}
/**
* @return {{{returnTypeHint}}}
*/
{{/if}}
public function {{methodName}}({{#each arguments as |argument|}}
{{>operation-argument argument}}{{#if @last}}
{{/if}}{{/each}}): {{returnType}} {
$closure = fn (?int $limit, ?int $offset): Collection => $this->{{originalMethodName}}({{#each arguments as |argument|}}
{{argument.name}}: ${{argument.name}},{{#if @last}}
{{/if}}{{/each}});

return new {{returnType}}(
$limit !== null || $offset !== null ? $closure(limit: $limit, offset: $offset) : null,
$closure,
);
}
5 changes: 5 additions & 0 deletions sdk-generator/templates/operation-pdf.php.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{#>operation-layout}}
$response = $this->client->send($request, ['allow_redirects' => ['refer' => true]]);

return $response->getBody();
{{/operation-layout}}
7 changes: 7 additions & 0 deletions sdk-generator/templates/static/gitignore.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.idea
vendor/
phpunit.xml
.php-cs-fixer.cache
.phpunit.result.cache
composer.lock
node_modules/
116 changes: 116 additions & 0 deletions sdk-generator/templates/static/php-cs-fixer.php.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
{{>file-header}}
declare(strict_types=1);

use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;

$header = <<<'EOF'
This source file is proprietary and part of Rebilly.

(c) Rebilly SRL
Rebilly Ltd.
Rebilly Inc.

@see https://www.rebilly.com
EOF;

$rules = [
'@PSR12' => true,
'align_multiline_comment' => true,
'array_syntax' => ['syntax' => 'short'],
'array_indentation' => true,
'blank_line_before_statement' => true,
'binary_operator_spaces' => true,
'cast_spaces' => true,
'class_attributes_separation' => true,
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'compact_nullable_typehint' => true,
'concat_space' => ['spacing' => 'one'],
'header_comment' => [
'header' => $header,
'comment_type' => 'PHPDoc',
'separate' => 'bottom',
'location' => 'after_open',
],
'heredoc_to_nowdoc' => true,
'general_phpdoc_annotation_remove' => ['annotations' => ['author', 'version']],
'list_syntax' => ['syntax' => 'short'],
'lowercase_cast' => true,
'magic_constant_casing' => true,
'mb_str_functions' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'modernize_types_casting' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block', 'switch', 'case', 'default']],
'no_homoglyph_names' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => true,
'no_multiline_whitespace_around_double_arrow' => true,
'no_null_property_initialization' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_around_offset' => true,
'no_superfluous_elseif' => true,
'no_trailing_comma_in_singleline' => true,
'no_unneeded_control_parentheses' => true,
'no_unneeded_curly_braces' => true,
'no_unneeded_final_method' => true,
'no_unreachable_default_argument_value' => true,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'normalize_index_brace' => true,
'ordered_class_elements' => true,
'ordered_imports' => true,
'php_unit_strict' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_order' => true,
'phpdoc_types_order' => true,
'protected_to_private' => true,
'return_type_declaration' => true,
'self_accessor' => true,
'semicolon_after_instruction' => true,
'short_scalar_cast' => true,
'single_line_comment_style' => true,
'single_quote' => true,
'standardize_not_equals' => true,
'strict_comparison' => true,
'strict_param' => true,
'ternary_to_null_coalescing' => true,
'trailing_comma_in_multiline' => ['elements' => []],
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'visibility_required' => ['elements' => ['property', 'method', 'const']],
'void_return' => true,
'yoda_style' => ['equal' => false, 'identical' => false],
'object_operator_without_whitespace' => true,
'ternary_operator_spaces' => true,
'fully_qualified_strict_types' => true,
'global_namespace_import' => [
'import_constants' => false,
'import_functions' => false,
'import_classes' => true,
],
'single_space_around_construct' => true,
];

$finder = (new Finder())
->exclude('vendor')
->in(__DIR__);

return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setFinder($finder)
->setRules($rules)
->setRiskyAllowed(true)
->setUsingCache(true);
Loading
Loading