Skip to content

Create Dart Implementation#431

Open
Fried-man wants to merge 40 commits into
cucumber:mainfrom
Fried-man:feature/dart_implementation
Open

Create Dart Implementation#431
Fried-man wants to merge 40 commits into
cucumber:mainfrom
Fried-man:feature/dart_implementation

Conversation

@Fried-man
Copy link
Copy Markdown

@Fried-man Fried-man commented May 22, 2026

🤔 What's changed?

Adds an initial Dart implementation for Cucumber Messages

⚡️ What's your motivation?

There is currently no officially maintained Dart implementation of Cucumber Messages. I'd like to use a Dart implementation to build out Gherkin dart support.

🏷️ What kind of change is this?

  • 📖 Documentation (improvements without changing code)
  • ⚡ New feature (non-breaking change which adds new behaviour)

♻️ Anything particular you want feedback on?

If the API surface needs updates to better conform with the other language implementations.

📋 Checklist:

  • I agree to respect and uphold the Cucumber Community Code of Conduct
  • I've changed the behaviour of the code
    • I have added/updated tests to cover my changes.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly.
  • Users should know about my change
    • I have added an entry to the "Unreleased" section of the CHANGELOG, linking to this pull request.

This text was originally generated from a template, then edited by hand. You can modify the template here.

@Fried-man Fried-man changed the title Feature/dart implementation Create Dart Implementation May 22, 2026
Copy link
Copy Markdown
Member

@mpkorstanje mpkorstanje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya, thanks for the PR!

I've given this a quick look for big problems. I didn't spot any, but I'll have to give it a more detailed look later.

Some general remarks:

  1. I assume you're aware of cucumber/gherkin#30 and cucumber/gherkin#29.

  2. We currently don't publish the Gherkin parser. I assume that is something you'd like to see happen in the future. That can be arranged though but might take some work.

  3. I see that you've chosen to set default values for all required fields. Perhaps copied from the JavaScript implementation. We are trying to get rid of those for JavaScript (#285). It would make sense not copy that mistake into Dart. Ideally the constructor would throw an exception when trying to create message with missing required fields.

  4. The generated messages sit in the same folder as the support code. This makes reviewing code quite annoying. And this may cause issues in the future if introduce a message that clashes with the support code. Can we separate these?

A few more detailed remarks below.

Comment thread codegen/generators/dart.rb Outdated
// ignore_for_file: cast_nullable_to_non_nullable, public_member_api_docs
// ignore_for_file: sort_constructors_first
// ignore_for_file: unnecessary_null_in_if_null_operators
HEADER
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, please move this to the template. That's where it is expected.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to where this is expected

Comment thread codegen/generators/dart.rb Outdated
def class_name(ref)
base = File.basename(ref, '.schema.json')
return 'DurationMessage' if base == 'Duration'
return 'ExceptionMessage' if base == 'Exception'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is necessary, this could do with a comment to explain why these names are different. Though I assume Dart has some name spacing to prevent collisions in the first place, if so then using the message names would be preferable. People very familiar with messages, but not so familiar with Dart will occasionally have to make small changes to implementations that use Dart. This would likely trip them up.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It collides with some foundational dart classes. I refactored to use the normal names & refactored code that depends on it with explicit references to the file the class was from. Ex:

import 'package:cucumber_messages/src/messages.dart' as messages;

/// Converts a Dart [Duration] to a Cucumber [messages.Duration].
///
/// The [duration] value is exported as whole seconds plus nanoseconds.
messages.Duration durationToDurationMessage(Duration duration) {
  final micros = duration.inMicroseconds;
  final seconds = micros ~/ Duration.microsecondsPerSecond;
  final nanos = micros.remainder(Duration.microsecondsPerSecond) * 1000;
  return messages.Duration(seconds: seconds, nanos: nanos);
}

part of 'messages.dart';

class DurationMessage {
final int seconds;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the range on integer values in dart?

Copy link
Copy Markdown
Author

@Fried-man Fried-man May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From first-party docs:

The default implementation of int is 64-bit two's complement integers with operations that wrap to that range on overflow.

Note: When compiling to JavaScript, integers are restricted to values that can be represented exactly by double-precision floating point values. The available integer values include all integers between -2^53 and 2^53, and some integers with larger magnitude. That includes some integers larger than 2^63. The behavior of the operators and methods in the int class therefore sometimes differs between the Dart VM and Dart code compiled to JavaScript. For example, the bitwise operators truncate their operands to 32-bit integers when compiled to JavaScript.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically:

Platform Underlying Representation Minimum Value Maximum Value
Native 64-bit signed integer (-2^{63}) (2^{63} - 1)
Web 64-bit float (JS) (-2^{53} + 1) (precise) (2^{53} - 1) (precise)

Comment thread dart/lib/src/id_generator.dart Outdated
}

/// Returns an [IdGenerator] that produces random RFC 4122 version 4 UUIDs.
IdGenerator uuidV4IdGenerator() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is a good idea to implement and maintain our own UUIDv4 generator. I also don't think it is a good idea to add a dependency on library that does this.

Ideally the IdGenerator is an interface that users of this library implement. For example by delegating to an existing UUIDv4 generator implementation that they pull in as a dependency.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, updated to only be an interface.

Comment thread dart/lib/src/ndjson_message_stream.dart Outdated
Envelope parseEnvelopeJson(
String json, {
int? lineNumber,
bool includeLineInErrors = true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please default this to false. While not likely, it avoids accidental information disclosure type issues as well as logging of untrusted data.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated default to false

Comment thread dart/CHANGELOG.md
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't duplicate the CHANGELOG in the root of the project. This one will be overlooked if we make changes.

Copy link
Copy Markdown
Author

@Fried-man Fried-man May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, removed

Copy link
Copy Markdown
Author

@Fried-man Fried-man May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, re-added but had it point to the root changelog. pub.dev (official dart package distributor) has a scoring system for packages and having a changelog is one of the criteria. While it is not required, it can make the package not appear well-maintained & there is a GUI for the changelog on the site for potential adopters to reference.

If you would like, I can remove it again but felt like having it serve to only reference the root changelog was a fine solution to prevent the issue of having to maintain multiple versions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, looks like they require I include the current version number in the changelog or else there will be an error when validating publishing. I included that validation for publishing dry run in the CI to ensure it stays publishable but can understand it be annoying to have to bump the changelog value whenever the package version changes.

Comment thread .github/workflows/test-dart.yml Outdated
matrix:
os:
- ubuntu-latest
dart: ["3.5.0", stable]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pin these to exact versions. This keeps CI stable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinned to exact versions

Comment thread dart/Makefile Outdated

.generate-messages: $(schemas) ../codegen/codegen.rb ../codegen/templates/dart.dart.erb
ruby ../codegen/codegen.rb Generator::Dart dart.dart.erb > Generated.dart.tmp
ruby -e "input = File.readlines('Generated.dart.tmp', chomp: true); current = nil; lines = []; flush = lambda { next unless current; File.write(File.join('lib/src', current), lines.join(\"\\n\") + \"\\n\"); lines = [] }; input.each do |line| if line.end_with?('.g.dart') && !line.include?(' '); flush.call; current = line; else; lines << line; end; end; flush.call"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this splits the generated file?

If so, please use the csplit solution used by other implementations.

messages/java/Makefile

Lines 21 to 25 in 521cb9d

csplit --quiet --prefix=Generated --suffix-format=%02d.java.tmp --elide-empty-files Generated.java.tmp /^[A-Za-z.]*[.]java/ {*}
rm Generated.java.tmp
rm -rf src/generated/java/io/cucumber/messages/types
mkdir --parents src/generated/java/io/cucumber/messages/types
for file in Generated**; do tail -n +2 $$file > src/generated/java/io/cucumber/messages/types/$$(head -n 1 $$file | tr -d '\r\n'); rm $$file; done

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to run using cpslit solution

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and yeah it does split the generated files

Comment thread .github/workflows/test-dart.yml Outdated
with:
persist-credentials: false

- uses: dart-lang/setup-dart@c7a31f6a04bb9bf94f538fc551f7ef9e40223317 # v1.7.1
Copy link
Copy Markdown
Member

@mpkorstanje mpkorstanje May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get these hashes from? Have you used an LLM?

I also don't see any actions ran in https://github.com/Fried-man/messages/actions. Please this yourself before submitting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I used an LLM and it hallucinated. Updated to a real hash.

I tried to run it manually from the actions tab on my fork but didn't see an option to. Realized this morning that I could just open a PR on my fork and run the CI there. Here is a passing run from that.

Comment thread .github/workflows/test-dart.yml Outdated
with:
persist-credentials: false

- uses: dart-lang/setup-dart@c7a31f6a04bb9bf94f538fc551f7ef9e40223317 # v1.7.1
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Comment thread dart/Makefile Outdated

require: ## Check requirements for generation and checks
@ruby --version >/dev/null 2>&1 || (echo "ERROR: ruby is required."; exit 1)
@dart --version >/dev/null 2>&1 || (echo "ERROR: dart is required."; exit 1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dart should not be required to generate the code. I suspect that will go away once the bench mark tests are removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed dart requirement. I was mainly using it to run dart format on the generated code. Not important to format generated code.

@Fried-man
Copy link
Copy Markdown
Author

Fried-man commented May 22, 2026

Heya, thanks for the PR!

I've given this a quick look for big problems. I didn't spot any, but I'll have to give it a more detailed look later.

Some general remarks:

  1. I assume you're aware of dart: Acceptance tests are not implemented gherkin#30 and dart: Parser is nog generated using Berp gherkin#29.

  2. We currently don't publish the Gherkin parser. I assume that is something you'd like to see happen in the future. That can be arranged though but might take some work.

  3. I see that you've chosen to set default values for all required fields. Perhaps copied from the JavaScript implementation. We are trying to get rid of those for JavaScript (Proposal to remove JavaScript classes and just generate interfaces #285). It would make sense not copy that mistake into Dart. Ideally the constructor would throw an exception when trying to create message with missing required fields.

  4. The generated messages sit in the same folder as the support code. This makes reviewing code quite annoying. And this may cause issues in the future if introduce a message that clashes with the support code. Can we separate these?

Hey! Thanks for the review

  1. I did see that. Planning on solving those.
  2. I would like the gherkin parser to be published. Totally understand it may take some work & for the short-term I can have code that depends on it point to the repo itself rather than pub.dev
  3. I was concerned that was going to be considered un-wanted behavior but thought it was fine since JS had it (Missed Proposal to remove JavaScript classes and just generate interfaces #285 ). I have updated it to require these values.
  4. Sure, I moved the generated files (*.g.dart) to dart\lib\src\generated.

@Fried-man Fried-man force-pushed the feature/dart_implementation branch from aedef0e to f42f0ee Compare May 22, 2026 12:09
@Fried-man
Copy link
Copy Markdown
Author

Alright, I think I'm done lol. Latest dart run passing on my fork can be found here.

Copy link
Copy Markdown
Member

@mpkorstanje mpkorstanje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I don't have time for another review pass today.

But I was reading https://pubrelease.onepub.dev/creating-a-release and I see that the pub_release command is rather involved and makes a lot of assumptions about the work flow.

Because we're releasing 10+ different languages we don't exactly have a standard process.

But it looks like this:

  1. Prepare the release by updating the version numbers and making other changes on a dev-machine.
  2. Tag these changes in Git.
  3. Push the changes and tags to GitHub.
  4. GitHub will trigger on the tags and start the release process for each action independently.

Can you tell me if the pub_release command is the only way to "publish your project to pub.dev"? We might have some experimenting to do to that get that work.

@Fried-man
Copy link
Copy Markdown
Author

Fried-man commented May 22, 2026

Can you tell me if the pub_release command is the only way to "publish your project to pub.dev"? We might have some experimenting to do to that get that work.

I believe the official publish path is dart pub publish. I don't believe it automatically creates tags or bumps the version.

@mpkorstanje mpkorstanje self-requested a review May 27, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants