Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

messages: PHP implementation #1884

Merged
merged 40 commits into from Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ed7105d
DTO generation in PHP
ciaranmcnulty Jan 13, 2022
d621ae0
JSON encoding and decoding in PHP
ciaranmcnulty Jan 13, 2022
eeb51f2
Additional validation and stricter type checks
ciaranmcnulty Jan 21, 2022
42c48df
Fix tabs->spaces
ciaranmcnulty Jan 21, 2022
9b90fac
Fix JSON decoding flags
ciaranmcnulty Jan 22, 2022
605a645
Ensure composer dependencies are installed before build
ciaranmcnulty Jan 22, 2022
c5142c7
Split generated classes into separate files
ciaranmcnulty Jan 22, 2022
dc7432c
Ensure final classes, strict types
ciaranmcnulty Jan 23, 2022
3000112
Stream readers and writers for NdJson input
ciaranmcnulty Jan 23, 2022
9a4f3b1
Clarify return type of Generator
ciaranmcnulty Jan 23, 2022
b989759
Add README.md
ciaranmcnulty Jan 25, 2022
1082381
Add clean target to Makefile
ciaranmcnulty Jan 26, 2022
5ba4a34
Additional validation rules
ciaranmcnulty Jan 26, 2022
83f6a85
Better formatting in README
ciaranmcnulty Jan 26, 2022
d6db891
Neater class hierarchy
ciaranmcnulty Jan 27, 2022
591d78c
Handle exceptions thrown by Message objects when deserialising
ciaranmcnulty Jan 27, 2022
32d078e
Fix psalm-assert for optional scalar keys
ciaranmcnulty Jan 27, 2022
f7a5cda
Add subrepo to messages/php
aurelien-reeves Jan 27, 2022
0689263
Allow Messages to be constructed
ciaranmcnulty Jan 27, 2022
ce973c8
Add coding style rules
ciaranmcnulty Jan 27, 2022
971a2f2
Add templates for php projects
aurelien-reeves Jan 28, 2022
02240b1
Add .rsync into messages/php
aurelien-reeves Jan 28, 2022
faa8b0c
Update default.mk php template
aurelien-reeves Jan 28, 2022
abd427d
Synchronize messages/php files
aurelien-reeves Jan 28, 2022
3f791db
Add messages/php to CI in parallel jobs (not serial)
aurelien-reeves Jan 28, 2022
e09bbfa
Fix circleci config.yml file
aurelien-reeves Jan 28, 2022
ea9bbab
Update php makefile
aurelien-reeves Jan 28, 2022
bd9f470
Fix make .tested target
aurelien-reeves Jan 28, 2022
4512643
Use cucumber build image v0.12
aurelien-reeves Jan 28, 2022
da2c254
Use new cucumber-build 0.11.0
aurelien-reeves Jan 28, 2022
c9ae102
Update php template .gitignore
aurelien-reeves Jan 28, 2022
506938c
Prevent overriding 'clean' make target
aurelien-reeves Jan 28, 2022
b50e70f
Merge branch 'main' into messages-php
aurelien-reeves Jan 31, 2022
1fafc55
Merge branch 'main' into messages-php
aurelien-reeves Jan 31, 2022
9430899
Fix codegen after merging main
aurelien-reeves Jan 31, 2022
4c6f647
Fix php enum codegen
aurelien-reeves Jan 31, 2022
977c9f6
Fix codegen after merging the one from main
aurelien-reeves Jan 31, 2022
b9e420f
Ignoring the whole 'build' directory from git
aurelien-reeves Jan 31, 2022
0e1097f
Fix gitignore sync issue
aurelien-reeves Jan 31, 2022
50afe00
Merge branch 'main' into messages-php
aurelien-reeves Jan 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions .circleci/config.yml
Expand Up @@ -66,6 +66,13 @@ executors:
docker:
- image: cimg/python:3.10.2
working_directory: ~/cucumber

# Php
docker-circleci-php:
docker:
- image: cimg/php:8.1.2
working_directory: ~/cucumber

###
### Jobs ###
###
Expand Down Expand Up @@ -677,6 +684,19 @@ jobs:
# cd gherkin/elixir
# make

### PHP

messages-php:
executor: docker-circleci-php
steps:
- attach_workspace:
at: '~/cucumber'
- run:
name: messages/php
command: |
cd messages/php
make

###
### Workflows ###
###
Expand Down Expand Up @@ -888,3 +908,9 @@ workflows:
# - gherkin-elixir:
# requires:
# - messages-elixir

### PHP

- messages-php:
requires:
- prepare-parallel
4 changes: 4 additions & 0 deletions .templates/php/.gitignore
@@ -0,0 +1,4 @@
vendor
composer.lock
.phpunit.cache
.php-cs-fixer.cache
51 changes: 51 additions & 0 deletions .templates/php/default.mk
@@ -0,0 +1,51 @@
# Please update /.templates/php/default.mk and sync:
#
# source scripts/functions.sh && rsync_files
#
SHELL := /usr/bin/env bash
PHP_SOURCE_FILES = $(shell find . -name "*.php")

### COMMON stuff for all platforms

### Common targets for all functionalities implemented on php

default: .tested
.PHONY: default

pre-release: update-version update-dependencies
.PHONY: pre-release

update-version:
ifdef NEW_VERSION
# TODO: something here?
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
endif
.PHONY: update-version

update-dependencies:
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
.PHONY: update-dependencies

publish:
# TODO: how to publish?
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
.PHONY: publish

post-release:
# no-op
.PHONY: post-release

clean:
rm -rf vendor composer.lock
.PHONY: clean

.tested: .deps .codegen $(PHP_SOURCE_FILES)
vendor/bin/phpunit
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
.PHONY: .tested

.deps: composer.lock
touch $@

.codegen:
touch $@

composer.lock: composer.json
composer install
touch $@
108 changes: 105 additions & 3 deletions messages/jsonschema/scripts/codegen.rb
Expand Up @@ -51,7 +51,7 @@ def default_value(parent_type_name, property_name, property)
elsif property['type'] == 'string'
if property['enum']
enum_type_name = type_for(parent_type_name, property_name, property)
"#{enum_type_name}.#{enum_constant(property['enum'][0])}"
default_enum(enum_type_name, property)
else
"''"
end
Expand All @@ -67,6 +67,10 @@ def default_value(parent_type_name, property_name, property)
end
end

def default_enum(enum_type_name, property)
"#{enum_type_name}.#{enum_constant(property['enum'][0])}"
end

def enum_constant(value)
value.gsub(/[\.\/\+]/, '_').upcase
end
Expand All @@ -76,6 +80,7 @@ def type_for(parent_type_name, property_name, property)
type = property['type']
items = property['items']
enum = property['enum']

if ref
property_type_from_ref(property['$ref'])
elsif type
Expand All @@ -84,8 +89,7 @@ def type_for(parent_type_name, property_name, property)
else
raise "No type mapping for JSONSchema type #{type}. Schema:\n#{JSON.pretty_generate(property)}" unless @language_type_by_schema_type[type]
if enum
enum_type_name = "#{parent_type_name}#{capitalize(property_name)}"
@enums.add({ name: enum_type_name, values: enum })
enum_type_name = enum_name(parent_type_name, property_name, enum)
property_type_from_enum(enum_type_name)
else
@language_type_by_schema_type[type]
Expand All @@ -105,6 +109,12 @@ def property_type_from_enum(enum)
enum
end

def enum_name(parent_type_name, property_name, enum)
enum_type_name = "#{parent_type_name}#{capitalize(property_name)}"
@enums.add({ name: enum_type_name, values: enum })
enum_type_name
end

def class_name(ref)
File.basename(ref, '.json')
end
Expand Down Expand Up @@ -295,6 +305,98 @@ def array_type_for(type_name)
end
end

class Php < Codegen
def initialize(paths)
template = File.read("#{TEMPLATES_DIRECTORY}/php.php.erb")
enum_template = File.read("#{TEMPLATES_DIRECTORY}/php.enum.php.erb")

language_type_by_schema_type = {
'string' => 'string',
'integer' => 'int',
'boolean' => 'bool',
}
super(paths, template, enum_template, language_type_by_schema_type)
end

def format_description(raw_description, indent_string: " ")
return '' if raw_description.nil?

raw_description
.split("\n")
.map { |line| line.strip() }
.filter { |line| line != '*' }
.map { |line| " * #{line}" }
.join("\n#{indent_string}")
end

def array_type_for(type_name)
"array"
end

def enum_name(parent_type_name, property_name, enum)
enum_type_name = "#{class_name(parent_type_name)}\\#{capitalize(property_name)}"
@enums.add({ name: enum_type_name, values: enum })
enum_type_name
end

def array_contents_type(parent_type_name, property_name, property)
type_for(parent_type_name, nil, property['items'])
end

def is_nullable(property_name, schema)
!(schema['required'] || []).index(property_name)
end

def is_scalar(property)
property.has_key?('type') && @language_type_by_schema_type.has_key?(property['type'])
end

def scalar_type_for(property)
raise "No type mapping for JSONSchema type #{type}. Schema:\n#{JSON.pretty_generate(property)}" unless @language_type_by_schema_type[property['type']]

@language_type_by_schema_type[property['type']]
end

def constructor_for(parent_type, property, property_name, schema, arr_name)
constr = non_nullable_constructor_for(parent_type, property, property_name, schema, arr_name)

is_nullable(property_name, schema) ? "isset($#{arr_name}['#{property_name}']) ? #{constr} : null" : constr
end

def non_nullable_constructor_for(parent_type, property, property_name, schema, arr_name)
source = property_name.nil? ? "#{arr_name}" : "#{arr_name}['#{property_name}']"
if is_scalar(property)
if property['enum']
"#{enum_name(parent_type, property_name, property['enum'])}::from((#{scalar_type_for(property)}) $#{source})"
else
"(#{scalar_type_for(property)}) $#{source}"
end
else
type = type_for(parent_type, property_name, property)
if type == 'array'
constructor = non_nullable_constructor_for(parent_type, property['items'], nil, schema, "member")
member_type = (property['items']['type'] ? 'mixed' : 'array')
"array_map(fn (#{member_type} $member) => #{constructor}, $#{source})"
else
"#{type_for(parent_type, property_name, property)}::fromArray($#{source})"
end
end
end

def default_value(class_name, property_name, property, schema)
if is_nullable(property_name, schema)
return 'null'
end

super(class_name, property_name, property)
end

def default_enum(enum_type_name, property)
"#{enum_type_name}::#{enum_constant(property['enum'][0])}"
end
end


clazz = Object.const_get(ARGV[0])
path = ARGV[1]
paths = File.file?(path) ? [path] : Dir["#{path}/*.json"]
Expand Down
12 changes: 12 additions & 0 deletions messages/jsonschema/scripts/templates/php.enum.php.erb
@@ -0,0 +1,12 @@
<%- namespaces = enum[:name].split('\\') -%>
namespace Cucumber\Messages\<%= namespaces.slice(0,1)[0] %>;

// CLASS_START <%= enum[:name].split('\\').join('/') %>.php
enum <%= namespaces[-1] %> : string
{
<%- enum[:values].each_with_index do |value, index| -%>
case <%= enum_constant(value) %> = '<%= value %>';
<%- end -%>
}

<%# block to preserve linebreaks %>
88 changes: 88 additions & 0 deletions messages/jsonschema/scripts/templates/php.php.erb
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

/**
* This code was auto-generated by {this script}[https://github.com/cucumber/common/blob/main/messages/jsonschema/scripts/codegen.rb]
*/

namespace Cucumber\Messages;

use JsonSerializable;
use Cucumber\Messages\DecodingException\SchemaViolationException;
<%- @schemas.sort.each do |key, schema| -%>
// CLASS_START <%= class_name(key) %>.php
/**
* Represents the <%= class_name(key) %> message in Cucumber's message protocol
* @see https://github.com/cucumber/common/tree/main/messages#readme
*
<%=- format_description(schema['description'], indent_string: '') -%> */
final class <%= class_name(key) %> implements JsonSerializable
{
use JsonEncodingTrait;

public function __construct(<%- schema['properties'].each do |property_name, property| -%>

<%- property_type = type_for(key, property_name, property) -%>
<%- if property['description'] or property_type == 'array' -%>
/**
<%- if property['description'] -%>
<%= format_description(property['description']) %>
<%- end -%>
<%- if property_type == 'array' -%>
* @param <%= is_nullable(property_name, schema) ? '?' : '' %>list<<%= array_contents_type(key, property_name, property) %>> $<%= property_name %>
<%- end -%>
*/
<%- end -%>
public readonly <%= is_nullable(property_name, schema) ? '?' : '' %><%= property_type %> $<%= property_name %> = <%= default_value(class_name(key), property_name, property, schema) -%>,
<%- end -%>
) {
}

/**
* @throws SchemaViolationException
*
* @internal
*/
public static function fromArray(array $arr): self
{
<%- schema['properties'].each do |property_name, property| -%>
self::ensure<%= capitalize(property_name)%>($arr);
<%- end -%>

return new self(
<%- schema['properties'].each do |property_name, property| -%>
<%= constructor_for(key, property, property_name, schema, 'arr') %>,
<%- end -%>
);
}
<%- schema['properties'].each do |property_name, property| -%>

/**
<%- if is_scalar(property) -%>
* @psalm-assert array{<%= property_name %><%- if (is_nullable(property_name, schema)) -%>?<%- end -%>: string|int|bool} $arr
<%- else -%>
* @psalm-assert array{<%= property_name %><%- if (is_nullable(property_name, schema)) -%>?<%- end -%>: array} $arr
<%- end -%>
*/
private static function ensure<%= capitalize(property_name)%>(array $arr): void
{
<%- if (!is_nullable(property_name, schema)) -%>
if (!array_key_exists('<%= property_name %>', $arr)) {
throw new SchemaViolationException('Property \'<%= property_name %>\' is required but was not found');
}
<%- end -%>
<%- if(is_scalar(property)) -%>
if (array_key_exists('<%= property_name %>', $arr) && is_array($arr['<%= property_name %>'])) {
throw new SchemaViolationException('Property \'<%= property_name %>\' was array');
}
<%- else -%>
if (array_key_exists('<%= property_name %>', $arr) && !is_array($arr['<%= property_name %>'])) {
throw new SchemaViolationException('Property \'<%= property_name %>\' was not array');
}
<%- end -%>
}
<%- end -%>
}

<% end %>
5 changes: 5 additions & 0 deletions messages/php/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,5 @@
PLEASE DO NOT CREATE ISSUES IN THIS REPO.
THIS REPO IS A READ-ONLY MIRROR.

Create your issue in the Cucumber monorepo instead:
https://github.com/cucumber/cucumber/issues
5 changes: 5 additions & 0 deletions messages/php/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,5 @@
PLEASE DO NOT CREATE PULL REAUESTS IN THIS REPO.
THIS REPO IS A READ-ONLY MIRROR.

Create your pull request in the Cucumber monorepo instead:
https://github.com/cucumber/cucumber/pulls
4 changes: 4 additions & 0 deletions messages/php/.gitignore
@@ -0,0 +1,4 @@
vendor
composer.lock
.phpunit.cache
.php-cs-fixer.cache
3 changes: 3 additions & 0 deletions messages/php/.rsync
@@ -0,0 +1,3 @@
../../LICENSE LICENSE
../../.templates/github/ .github/
../../.templates/php/ .
1 change: 1 addition & 0 deletions messages/php/.subrepo
@@ -0,0 +1 @@
cucumber/messages-php
21 changes: 21 additions & 0 deletions messages/php/LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) Cucumber Ltd

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.