From 27170f6e114824068194d1bb3c196a5688b65019 Mon Sep 17 00:00:00 2001 From: Matt Hooks <46452201+matt-dutchie@users.noreply.github.com> Date: Fri, 10 Mar 2023 10:11:50 -0600 Subject: [PATCH 01/24] fix: Use `content_type.mime_type` instead of direct header access (#36) In some environments, retrieving the `content-type` header directly from the headers hash was returning an empty string. In those environments, the header was stored as `Content-Type`. By updating the code to use the provided `content_type` method provided on the response object, we can shield ourselves from casing issues like this in the future. --- lib/ld-eventsource/client.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index 7c14eb7..e677446 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -85,7 +85,7 @@ class Client # if you want to use something other than the default `TCPSocket`; it must implement # `open(uri, timeout)` to return a connected `Socket` # @yieldparam [Client] client the new client instance, before opening the connection - # + # def initialize(uri, headers: {}, connect_timeout: DEFAULT_CONNECT_TIMEOUT, @@ -107,7 +107,7 @@ def initialize(uri, if socket_factory http_client_options["socket_class"] = socket_factory end - + if proxy @proxy = proxy else @@ -202,12 +202,12 @@ def closed? end private - + def reset_http @http_client.close if !@http_client.nil? close_connection end - + def close_connection @lock.synchronize do @cxn.connection.close if !@cxn.nil? @@ -258,7 +258,7 @@ def connect interval = @first_attempt ? 0 : @backoff.next_interval @first_attempt = false if interval > 0 - @logger.info { "Will retry connection after #{'%.3f' % interval} seconds" } + @logger.info { "Will retry connection after #{'%.3f' % interval} seconds" } sleep(interval) end cxn = nil @@ -268,14 +268,14 @@ def connect headers: build_headers }) if cxn.status.code == 200 - content_type = cxn.headers["content-type"] + content_type = cxn.content_type.mime_type if content_type && content_type.start_with?("text/event-stream") return cxn # we're good to proceed else reset_http - err = Errors::HTTPContentTypeError.new(cxn.headers["content-type"]) + err = Errors::HTTPContentTypeError.new(content_type) @on[:error].call(err) - @logger.warn { "Event source returned unexpected content type '#{cxn.headers["content-type"]}'" } + @logger.warn { "Event source returned unexpected content type '#{content_type}'" } end else body = cxn.to_s # grab the whole response body in case it has error details @@ -309,7 +309,7 @@ def read_stream(cxn) # readpartial gives us a string, which may not be a valid UTF-8 string because a # multi-byte character might not yet have been fully read, but BufferedLineReader # will handle that. - rescue HTTP::TimeoutError + rescue HTTP::TimeoutError # For historical reasons, we rethrow this as our own type raise Errors::ReadTimeoutError.new(@read_timeout) end @@ -344,7 +344,7 @@ def log_and_dispatch_error(e, message) @logger.warn { "#{message}: #{e.inspect}"} @logger.debug { "Exception trace: #{e.backtrace}" } begin - @on[:error].call(e) + @on[:error].call(e) rescue StandardError => ee @logger.warn { "Error handler threw an exception: #{ee.inspect}"} @logger.debug { "Exception trace: #{ee.backtrace}" } From caa02080f990a0ce1496b6f51846da5c08d1e01e Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 13 Mar 2023 11:47:05 -0400 Subject: [PATCH 02/24] build: Update releaser base image (#37) --- .ldrelease/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml index ab2ac1f..4ba6592 100644 --- a/.ldrelease/config.yml +++ b/.ldrelease/config.yml @@ -17,6 +17,6 @@ branches: jobs: - docker: - image: ruby:2.5-buster + image: ruby:2.6-buster template: name: ruby From 709eb2d7b9a3eb13711dd12f66becade66481253 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Mon, 13 Mar 2023 15:49:23 +0000 Subject: [PATCH 03/24] Releasing version 2.2.2 --- CHANGELOG.md | 4 ++++ lib/ld-eventsource/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fbb7f2..95b5459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.2] - 2023-03-13 +### Fixed: +- Content-Type checking was failing in some environments due to casing issues. Updated check to use a more robust header retrieval method. (Thanks, [matt-dutchie](https://github.com/launchdarkly/ruby-eventsource/pull/36)!) + ## [2.2.1] - 2022-06-15 ### Fixed: - Improved efficiency of SSE parsing to reduce transient memory/CPU usage spikes when streams contain long lines. (Thanks, [sq-square](https://github.com/launchdarkly/ruby-eventsource/pull/32)!) diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 806179d..418f5f9 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.1" + VERSION = "2.2.2" end From 725200009c072e3190182b9d68f14c8b518c481a Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 13 Mar 2023 14:04:32 -0400 Subject: [PATCH 04/24] build: Add Ruby 3.1, 3.2, and JRuby 9.4 to the CI matrix. (#39) --- .circleci/config.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 54a42de..a7d5482 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,12 @@ workflows: - build-test-linux: name: Ruby 3.0 docker-image: cimg/ruby:3.0 + - build-test-linux: + name: Ruby 3.1 + docker-image: cimg/ruby:3.1 + - build-test-linux: + name: Ruby 3.2 + docker-image: cimg/ruby:3.2 - build-test-linux: name: JRuby 9.2 docker-image: jruby:9.2-jdk @@ -26,6 +32,11 @@ workflows: docker-image: jruby:9.3-jdk jruby: true skip-end-to-end-http-tests: "y" # webrick doesn't work reliably in JRuby + - build-test-linux: + name: JRuby 9.4 + docker-image: jruby:9.4-jdk + jruby: true + skip-end-to-end-http-tests: "y" # webrick doesn't work reliably in JRuby jobs: build-test-linux: From a6b00df32d549f88c248dac204af3a3aac6c8a82 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 1 Dec 2023 13:04:28 -0500 Subject: [PATCH 05/24] ci: Replace releaser with release please (#41) --- .circleci/config.yml | 78 ---------------------------- .github/actions/ci/action.yml | 27 ++++++++++ .github/actions/publish/action.yml | 18 +++++++ .github/workflows/ci.yml | 36 +++++++++++++ .github/workflows/lint-pr-title.yml | 12 +++++ .github/workflows/manual-publish.yml | 36 +++++++++++++ .github/workflows/release-please.yml | 43 +++++++++++++++ .ldrelease/config.yml | 22 -------- .release-please-manifest.json | 3 ++ README.md | 3 +- release-please-config.json | 11 ++++ 11 files changed, 188 insertions(+), 101 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/actions/ci/action.yml create mode 100644 .github/actions/publish/action.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lint-pr-title.yml create mode 100644 .github/workflows/manual-publish.yml create mode 100644 .github/workflows/release-please.yml delete mode 100644 .ldrelease/config.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index a7d5482..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,78 +0,0 @@ -version: 2.1 - -workflows: - version: 2 - test: - jobs: - - build-test-linux: - name: Ruby 2.5 - docker-image: cimg/ruby:2.5 - - build-test-linux: - name: Ruby 2.6 - docker-image: cimg/ruby:2.6 - - build-test-linux: - name: Ruby 2.7 - docker-image: cimg/ruby:2.7 - - build-test-linux: - name: Ruby 3.0 - docker-image: cimg/ruby:3.0 - - build-test-linux: - name: Ruby 3.1 - docker-image: cimg/ruby:3.1 - - build-test-linux: - name: Ruby 3.2 - docker-image: cimg/ruby:3.2 - - build-test-linux: - name: JRuby 9.2 - docker-image: jruby:9.2-jdk - jruby: true - skip-end-to-end-http-tests: "y" # webrick doesn't work reliably in JRuby - - build-test-linux: - name: JRuby 9.3 - docker-image: jruby:9.3-jdk - jruby: true - skip-end-to-end-http-tests: "y" # webrick doesn't work reliably in JRuby - - build-test-linux: - name: JRuby 9.4 - docker-image: jruby:9.4-jdk - jruby: true - skip-end-to-end-http-tests: "y" # webrick doesn't work reliably in JRuby - -jobs: - build-test-linux: - parameters: - docker-image: - type: string - jruby: - type: boolean - default: false - skip-end-to-end-http-tests: - type: string - default: "" - - docker: - - image: <> - environment: - LD_SKIP_END_TO_END_HTTP_TESTS: <> - - steps: - - checkout - - when: - condition: <> - steps: - - run: gem install jruby-openssl # required by bundler, no effect on Ruby MRI - - run: apt-get update -y && apt-get install -y build-essential - - when: - condition: - not: <> - steps: - - run: sudo apt-get update -y && sudo apt-get install -y build-essential - - run: ruby -v - - run: gem install bundler -v 2.2.10 - - run: bundle _2.2.10_ install - - run: mkdir ./rspec - - run: bundle _2.2.10_ exec rspec --format documentation --format RspecJunitFormatter -o ./rspec/rspec.xml spec - - store_test_results: - path: ./rspec - - store_artifacts: - path: ./rspec diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml new file mode 100644 index 0000000..9eb3adb --- /dev/null +++ b/.github/actions/ci/action.yml @@ -0,0 +1,27 @@ +name: CI Workflow +description: 'Shared CI workflow.' +inputs: + ruby-version: + description: 'The version of ruby to setup and run' + required: true + +runs: + using: composite + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby-version }} + bundler: 2.2.10 + + - name: Install dependencies + shell: bash + run: bundle _2.2.10_ install + + - name: Skip end to end tests for jruby + if: ${{ startsWith(inputs.ruby-version, 'jruby') }} + shell: bash + run: echo "LD_SKIP_END_TO_END_HTTP_TEST='y'" >> $GITHUB_ENV + + - name: Run tests + shell: bash + run: bundle _2.2.10_ exec rspec spec diff --git a/.github/actions/publish/action.yml b/.github/actions/publish/action.yml new file mode 100644 index 0000000..b1cb2c1 --- /dev/null +++ b/.github/actions/publish/action.yml @@ -0,0 +1,18 @@ +name: Publish Package +description: 'Publish the package to rubygems' +inputs: + dry_run: + description: 'Is this a dry run. If so no package will be published.' + required: true + +runs: + using: composite + steps: + - name: Build gemspec + shell: bash + run: gem build ld-eventsource.gemspec + + - name: Publish Library + shell: bash + if: ${{ inputs.dry_run == 'false' }} + run: gem push ld-eventsource-*.gem diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2d92aab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Run CI +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' # Do not need to run CI for markdown changes. + pull_request: + branches: [ main ] + paths-ignore: + - '**.md' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + ruby-version: + - '2.5' + - '2.6' + - '2.7' + - '3.0' + - '3.1' + - '3.2' + - jruby-9.2 + - jruby-9.3 + - jruby-9.4 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # If you only need the current version keep this. + + - uses: ./.github/actions/ci + with: + ruby-version: ${{ matrix.ruby-version }} diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml new file mode 100644 index 0000000..4ba79c1 --- /dev/null +++ b/.github/workflows/lint-pr-title.yml @@ -0,0 +1,12 @@ +name: Lint PR title + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + lint-pr-title: + uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml new file mode 100644 index 0000000..0133f7a --- /dev/null +++ b/.github/workflows/manual-publish.yml @@ -0,0 +1,36 @@ +name: Publish Package +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Is this a dry run. If so no package will be published.' + type: boolean + required: true + +jobs: + build-publish: + runs-on: ubuntu-latest + # Needed to get tokens during publishing. + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.0 + name: 'Get rubygems API key' + with: + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} + ssm_parameter_pairs: '/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY' + + - id: build-and-test + name: Build and Test + uses: ./.github/actions/ci + with: + ruby-version: 2.6 + + - id: publish + name: Publish Package + uses: ./.github/actions/publish + with: + dry_run: ${{ inputs.dry_run }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..cf1b5bd --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,43 @@ +name: Run Release Please + +on: + push: + branches: + - main + +jobs: + release-package: + runs-on: ubuntu-latest + permissions: + id-token: write # Needed if using OIDC to get release secrets. + contents: write # Contents and pull-requests are for release-please to make releases. + pull-requests: write + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + command: manifest + token: ${{secrets.GITHUB_TOKEN}} + default-branch: main + + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.releases_created }} + with: + fetch-depth: 0 # If you only need the current version keep this. + + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.0 + if: ${{ steps.release.outputs.releases_created }} + name: 'Get rubygems API key' + with: + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} + ssm_parameter_pairs: '/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY' + + - uses: ./.github/actions/ci + if: ${{ steps.release.outputs.releases_created }} + with: + ruby-version: 2.6 + + - uses: ./.github/actions/publish + if: ${{ steps.release.outputs.releases_created }} + with: + dry_run: false diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml deleted file mode 100644 index 4ba6592..0000000 --- a/.ldrelease/config.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: 2 - -repo: - public: ruby-eventsource - -publications: - - url: https://rubygems.org/gems/ld-eventsource - description: RubyGems - - url: https://www.rubydoc.info/gems/ld-eventsource - description: documentation - -branches: - - name: main - description: 2.x - based on the http gem - - name: 1.x - description: 1.x - based on the socketry gem - -jobs: - - docker: - image: ruby:2.6-buster - template: - name: ruby diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..d15f5ed --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2.2.2" +} diff --git a/README.md b/README.md index 544331f..2b6e13c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ LaunchDarkly SSE Client for Ruby ================================ -[![Gem Version](https://badge.fury.io/rb/ld-eventsource.svg)](http://badge.fury.io/rb/ld-eventsource) [![Circle CI](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/main.svg?style=svg)](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/main) +[![Gem Version](https://badge.fury.io/rb/ld-eventsource.svg)](http://badge.fury.io/rb/ld-eventsource) +[![Run CI](https://github.com/launchdarkly/ruby-eventsource/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/ruby-eventsource/actions/workflows/ci.yml) A client for the [Server-Sent Events](https://www.w3.org/TR/eventsource/) protocol. This implementation runs on a worker thread, and uses the [`http`](https://rubygems.org/gems/http) gem to manage a persistent connection. Its primary purpose is to support the [LaunchDarkly SDK for Ruby](https://github.com/launchdarkly/ruby-client), but it can be used independently. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..9c5e3af --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,11 @@ +{ + "packages": { + ".": { + "release-type": "ruby", + "bump-minor-pre-major": true, + "versioning": "default", + "include-component-in-tag": false, + "include-v-in-tag": false + } + } +} From 3de13f4c26d7f1ab89fcc08c7547596aba990d36 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 31 Jan 2024 11:49:58 -0500 Subject: [PATCH 06/24] build: Update version.rb during release process (#42) --- .github/actions/ci/action.yml | 2 +- lib/ld-eventsource/version.rb | 2 +- release-please-config.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 9eb3adb..cbaf94b 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -20,7 +20,7 @@ runs: - name: Skip end to end tests for jruby if: ${{ startsWith(inputs.ruby-version, 'jruby') }} shell: bash - run: echo "LD_SKIP_END_TO_END_HTTP_TEST='y'" >> $GITHUB_ENV + run: echo "LD_SKIP_END_TO_END_HTTP_TESTS='y'" >> $GITHUB_ENV - name: Run tests shell: bash diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 418f5f9..36c090f 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.2" + VERSION = "2.2.2" # x-release-please-version end diff --git a/release-please-config.json b/release-please-config.json index 9c5e3af..1bf4557 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,7 +5,8 @@ "bump-minor-pre-major": true, "versioning": "default", "include-component-in-tag": false, - "include-v-in-tag": false + "include-v-in-tag": false, + "extra-files": ["lib/ld-eventsource/version.rb"] } } } From 3dd93df4db3c0b3251d64411ab4546c1854f81e0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 31 Jan 2024 08:53:14 -0800 Subject: [PATCH 07/24] ci: Add SSE contract test support (#25) --- .github/actions/ci/action.yml | 5 +++ Makefile | 19 ++++++++ contract-tests/Gemfile | 10 +++++ contract-tests/README.md | 5 +++ contract-tests/service.rb | 77 +++++++++++++++++++++++++++++++++ contract-tests/stream_entity.rb | 58 +++++++++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 Makefile create mode 100644 contract-tests/Gemfile create mode 100644 contract-tests/README.md create mode 100644 contract-tests/service.rb create mode 100644 contract-tests/stream_entity.rb diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index cbaf94b..4293ff7 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -25,3 +25,8 @@ runs: - name: Run tests shell: bash run: bundle _2.2.10_ exec rspec spec + + - name: Run contract tests + if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} + shell: bash + run: make contract-tests diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..34a27c6 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log + +build-contract-tests: + @cd contract-tests && bundle _2.2.10_ install + +start-contract-test-service: + @cd contract-tests && bundle _2.2.10_ exec ruby service.rb + +start-contract-test-service-bg: + @echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" + @make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & + +run-contract-tests: + @curl -s https://raw.githubusercontent.com/launchdarkly/sse-contract-tests/v1.0.0/downloader/run.sh \ + | VERSION=v1 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end" sh + +contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests + +.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests diff --git a/contract-tests/Gemfile b/contract-tests/Gemfile new file mode 100644 index 0000000..84bdd56 --- /dev/null +++ b/contract-tests/Gemfile @@ -0,0 +1,10 @@ +source 'https://rubygems.org' + +gem 'ld-eventsource', path: '..' + +gem 'sinatra', '~> 2.1' +# Sinatra can work with several server frameworks. In JRuby, we have to use glassfish (which +# is only available in JRuby). Otherwise we use thin (which is not available in JRuby). +gem 'glassfish', :platforms => :jruby +gem 'thin', :platforms => :ruby +gem 'json' diff --git a/contract-tests/README.md b/contract-tests/README.md new file mode 100644 index 0000000..37cc97c --- /dev/null +++ b/contract-tests/README.md @@ -0,0 +1,5 @@ +# SSE client contract test service + +This directory contains an implementation of the cross-platform SSE testing protocol defined by https://github.com/launchdarkly/sse-contract-tests. See that project's `README` for details of this protocol, and the kinds of SSE client capabilities that are relevant to the contract tests. This code should not need to be updated unless the SSE client has added or removed such capabilities. + +To run these tests locally, run `make contract-tests` from the project root directory. This downloads the correct version of the test harness tool automatically. diff --git a/contract-tests/service.rb b/contract-tests/service.rb new file mode 100644 index 0000000..839822c --- /dev/null +++ b/contract-tests/service.rb @@ -0,0 +1,77 @@ +require 'ld-eventsource' +require 'json' +require 'logger' +require 'net/http' +require 'sinatra' + +require './stream_entity.rb' + +$log = Logger.new(STDOUT) +$log.formatter = proc {|severity, datetime, progname, msg| + "#{datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')} #{severity} #{progname} #{msg}\n" +} + +set :port, 8000 +set :logging, false + +streams = {} +streamCounter = 0 + +get '/' do + { + capabilities: [ + 'headers', + 'last-event-id', + 'read-timeout' + ] + }.to_json +end + +delete '/' do + $log.info("Test service has told us to exit") + Thread.new { sleep 1; exit } + return 204 +end + +post '/' do + opts = JSON.parse(request.body.read, :symbolize_names => true) + streamUrl = opts[:streamUrl] + callbackUrl = opts[:callbackUrl] + tag = "[#{opts[:tag]}]:" + + if !streamUrl || !callbackUrl + $log.error("#{tag} Received request with incomplete parameters: #{opts}") + return 400 + end + + streamCounter += 1 + streamId = streamCounter.to_s + streamResourceUrl = "/streams/#{streamId}" + + $log.info("#{tag} Starting stream from #{streamUrl}") + $log.debug("#{tag} Parameters: #{opts}") + + entity = nil + sse = SSE::Client.new( + streamUrl, + headers: opts[:headers] || {}, + last_event_id: opts[:lastEventId], + read_timeout: opts[:readTimeoutMs].nil? ? nil : (opts[:readTimeoutMs].to_f / 1000), + reconnect_time: opts[:initialDelayMs].nil? ? nil : (opts[:initialDelayMs].to_f / 1000) + ) do |sse| + entity = StreamEntity.new(sse, tag, callbackUrl) + end + + streams[streamId] = entity + + return [201, {"Location" => streamResourceUrl}, nil] +end + +delete '/streams/:id' do |streamId| + entity = streams[streamId] + return 404 if entity.nil? + streams.delete(streamId) + entity.close + + return 204 +end diff --git a/contract-tests/stream_entity.rb b/contract-tests/stream_entity.rb new file mode 100644 index 0000000..0c62543 --- /dev/null +++ b/contract-tests/stream_entity.rb @@ -0,0 +1,58 @@ +require 'ld-eventsource' +require 'json' +require 'net/http' + +set :port, 8000 +set :logging, false + +class StreamEntity + def initialize(sse, tag, callbackUrl) + @sse = sse + @tag = tag + @callbackUrl = callbackUrl + @callbackCounter = 0 + + sse.on_event { |event| self.on_event(event) } + sse.on_error { |error| self.on_error(error) } + end + + def on_event(event) + $log.info("#{@tag} Received event from stream (#{event.type})") + message = { + kind: 'event', + event: { + type: event.type, + data: event.data, + id: event.last_event_id + } + } + self.send_message(message) + end + + def on_error(error) + $log.info("#{@tag} Received error from stream: #{error}") + message = { + kind: 'error', + error: error + } + self.send_message(message) + end + + def send_message(message) + @callbackCounter += 1 + uri = "#{@callbackUrl}/#{@callbackCounter}" + begin + resp = Net::HTTP.post(URI(uri), JSON.generate(message)) + if resp.code.to_i >= 300 + $log.error("#{@tag} Callback to #{url} returned status #{resp.code}") + end + rescue => e + $log.error("#{@tag} Callback to #{url} failed: #{e}") + end + end + + def close + @sse.close + $log.info("#{@tag} Test ended") + end +end From a97391f9981037360620b63c1c9dc1f8ffc40f8d Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 13 Aug 2024 17:22:44 -0400 Subject: [PATCH 08/24] chore: Add missing CODEOWNERS file (#45) --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..71f97d0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Repository Maintainers +* @launchdarkly/team-sdk-ruby From de041bd87facd319e8287ca4257f40bb136f9de7 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 13 Aug 2024 17:23:12 -0400 Subject: [PATCH 09/24] ci: Update various github action versions (#44) --- .github/workflows/manual-publish.yml | 2 +- .github/workflows/release-please.yml | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 0133f7a..d5afced 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.0 + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 name: 'Get rubygems API key' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index cf1b5bd..d0f33b8 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -13,31 +13,27 @@ jobs: contents: write # Contents and pull-requests are for release-please to make releases. pull-requests: write steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v4 id: release - with: - command: manifest - token: ${{secrets.GITHUB_TOKEN}} - default-branch: main - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.releases_created }} + if: ${{ steps.release.outputs.releases_created == 'true' }} with: fetch-depth: 0 # If you only need the current version keep this. - - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.0 - if: ${{ steps.release.outputs.releases_created }} + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 + if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get rubygems API key' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} ssm_parameter_pairs: '/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY' - uses: ./.github/actions/ci - if: ${{ steps.release.outputs.releases_created }} + if: ${{ steps.release.outputs.releases_created == 'true' }} with: ruby-version: 2.6 - uses: ./.github/actions/publish - if: ${{ steps.release.outputs.releases_created }} + if: ${{ steps.release.outputs.releases_created == 'true' }} with: dry_run: false From 191fd68f539447fda22c4cbcdfe575984658780a Mon Sep 17 00:00:00 2001 From: Yuki Nishijima <386234+yuki24@users.noreply.github.com> Date: Sat, 8 Mar 2025 03:26:20 +0900 Subject: [PATCH 10/24] fix: Provide thread name for inspection (#46) --- .github/workflows/ci.yml | 2 +- lib/ld-eventsource/client.rb | 4 +--- scripts/gendocs.sh | 12 ------------ scripts/release.sh | 30 ------------------------------ 4 files changed, 2 insertions(+), 46 deletions(-) delete mode 100755 scripts/gendocs.sh delete mode 100755 scripts/release.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d92aab..0d33d58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: ruby-version: - '2.5' @@ -22,7 +23,6 @@ jobs: - '3.0' - '3.1' - '3.2' - - jruby-9.2 - jruby-9.3 - jruby-9.4 diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index e677446..6355a62 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -141,9 +141,7 @@ def initialize(uri, yield self if block_given? - Thread.new do - run_stream - end + Thread.new { run_stream }.name = 'LD/SSEClient' end # diff --git a/scripts/gendocs.sh b/scripts/gendocs.sh deleted file mode 100755 index 45ff28c..0000000 --- a/scripts/gendocs.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Use this script to generate documentation locally in ./doc so it can be proofed before release. -# After release, documentation will be visible at https://www.rubydoc.info/gems/ld-eventsource - -gem install --conservative yard -gem install --conservative redcarpet # provides Markdown formatting - -# yard doesn't seem to do recursive directories, even though Ruby's Dir.glob supposedly recurses for "**" -PATHS="lib/*.rb lib/**/*.rb lib/**/**/*.rb" - -yard doc --no-private --markup markdown --markup-provider redcarpet --embed-mixins $PATHS - README.md diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index 81aac06..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# This script updates the version for the library and releases it to RubyGems -# It will only work if you have the proper credentials set up in ~/.gem/credentials - -# It takes exactly one argument: the new version. -# It should be run from the root of this git repo like this: -# ./scripts/release.sh 4.0.9 - -# When done you should commit and push the changes made. - -set -uxe - -VERSION=$1 -GEM_NAME=ld-eventsource - -echo "Starting $GEM_NAME release." - -# Update version in version.rb -VERSION_RB_TEMP=./version.rb.tmp -sed "s/VERSION =.*/VERSION = \"${VERSION}\"/g" lib/$GEM_NAME/version.rb > ${VERSION_RB_TEMP} -mv ${VERSION_RB_TEMP} lib/$GEM_NAME/version.rb - -# Build Ruby gem -gem build $GEM_NAME.gemspec - -# Publish Ruby gem -gem push $GEM_NAME-${VERSION}.gem - -echo "Done with $GEM_NAME release" \ No newline at end of file From 455445cc64c0fb13df6b1c2a5a228af89037b4e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:34:26 -0500 Subject: [PATCH 11/24] chore(main): release 2.2.3 (#48) :robot: I have created a release *beep* *boop* --- ## [2.2.3](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.2...2.2.3) (2025-03-07) ### Bug Fixes * Provide thread name for inspection ([#46](https://github.com/launchdarkly/ruby-eventsource/issues/46)) ([191fd68](https://github.com/launchdarkly/ruby-eventsource/commit/191fd68f539447fda22c4cbcdfe575984658780a)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ lib/ld-eventsource/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d15f5ed..9485046 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.2" + ".": "2.2.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b5459..becc439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.3](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.2...2.2.3) (2025-03-07) + + +### Bug Fixes + +* Provide thread name for inspection ([#46](https://github.com/launchdarkly/ruby-eventsource/issues/46)) ([191fd68](https://github.com/launchdarkly/ruby-eventsource/commit/191fd68f539447fda22c4cbcdfe575984658780a)) + ## [2.2.2] - 2023-03-13 ### Fixed: - Content-Type checking was failing in some environments due to casing issues. Updated check to use a more robust header retrieval method. (Thanks, [matt-dutchie](https://github.com/launchdarkly/ruby-eventsource/pull/36)!) diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 36c090f..40ce3e3 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.2" # x-release-please-version + VERSION = "2.2.3" # x-release-please-version end From 52a9c64922e012b40c240f3502a1a94eed07d4fc Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 7 Mar 2025 15:59:40 -0500 Subject: [PATCH 12/24] chore: Explicitly provide access to GEM_HOST_API_KEY (#49) --- .github/workflows/manual-publish.yml | 2 ++ .github/workflows/release-please.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index d5afced..c50828c 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -34,3 +34,5 @@ jobs: uses: ./.github/actions/publish with: dry_run: ${{ inputs.dry_run }} + env: + GEM_HOST_API_KEY: ${{ env.GEM_HOST_API_KEY }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index d0f33b8..4403cd8 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -37,3 +37,5 @@ jobs: if: ${{ steps.release.outputs.releases_created == 'true' }} with: dry_run: false + env: + GEM_HOST_API_KEY: ${{ env.GEM_HOST_API_KEY }} From 95185a61766ac7586116b1da8546b42e2ad1e726 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Fri, 21 Mar 2025 13:05:32 -0400 Subject: [PATCH 13/24] chore: Bump ruby version to support GEM_HOST_API_KEY (#50) --- .github/workflows/manual-publish.yml | 2 +- .github/workflows/release-please.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index c50828c..770d4c5 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -27,7 +27,7 @@ jobs: name: Build and Test uses: ./.github/actions/ci with: - ruby-version: 2.6 + ruby-version: '3.0' - id: publish name: Publish Package diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 4403cd8..d55f8fa 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -31,7 +31,7 @@ jobs: - uses: ./.github/actions/ci if: ${{ steps.release.outputs.releases_created == 'true' }} with: - ruby-version: 2.6 + ruby-version: '3.0' - uses: ./.github/actions/publish if: ${{ steps.release.outputs.releases_created == 'true' }} From 8be0ccc1572aa6600e03833ac3d37a231b4c14f9 Mon Sep 17 00:00:00 2001 From: Kirill Usanov <48535087+Exterm1nate@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:43:42 +0300 Subject: [PATCH 14/24] fix: Remove rake dependency from gemspec (#53) --- ld-eventsource.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ld-eventsource.gemspec b/ld-eventsource.gemspec index 730da09..d671bc4 100644 --- a/ld-eventsource.gemspec +++ b/ld-eventsource.gemspec @@ -3,7 +3,6 @@ lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "ld-eventsource/version" -require "rake" # rubocop:disable Metrics/BlockLength Gem::Specification.new do |spec| @@ -16,7 +15,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/launchdarkly/ruby-eventsource" spec.license = "Apache-2.0" - spec.files = FileList["lib/**/*", "README.md", "LICENSE"] + spec.files = Dir.glob("lib/**/*") + ["README.md", "LICENSE"] spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From a74139be68d748ed21319ecbf312a5991dc9855c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:36:55 -0400 Subject: [PATCH 15/24] chore(main): release 2.2.4 (#55) :robot: I have created a release *beep* *boop* --- ## [2.2.4](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.3...2.2.4) (2025-04-18) ### Bug Fixes * Remove rake dependency from gemspec ([#53](https://github.com/launchdarkly/ruby-eventsource/issues/53)) ([8be0ccc](https://github.com/launchdarkly/ruby-eventsource/commit/8be0ccc1572aa6600e03833ac3d37a231b4c14f9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ lib/ld-eventsource/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9485046..3a3df6f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.3" + ".": "2.2.4" } diff --git a/CHANGELOG.md b/CHANGELOG.md index becc439..18b905d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.4](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.3...2.2.4) (2025-04-18) + + +### Bug Fixes + +* Remove rake dependency from gemspec ([#53](https://github.com/launchdarkly/ruby-eventsource/issues/53)) ([8be0ccc](https://github.com/launchdarkly/ruby-eventsource/commit/8be0ccc1572aa6600e03833ac3d37a231b4c14f9)) + ## [2.2.3](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.2...2.2.3) (2025-03-07) diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 40ce3e3..7f3c615 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.3" # x-release-please-version + VERSION = "2.2.4" # x-release-please-version end From 93a994783aa3aa922a213670a3c6183206d8bd8d Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 14 Jul 2025 12:43:22 -0400 Subject: [PATCH 16/24] fix: Bump minimum to ruby 3.1 (#57) --- .github/actions/ci/action.yml | 35 +- .github/workflows/ci.yml | 20 +- .github/workflows/manual-publish.yml | 9 +- .github/workflows/release-please.yml | 7 +- .rubocop.yml | 889 ++++++++++++++++++++++++ Makefile | 4 +- README.md | 2 +- contract-tests/Gemfile | 8 +- contract-tests/service.rb | 8 +- contract-tests/stream_entity.rb | 6 +- ld-eventsource.gemspec | 38 +- lib/ld-eventsource/client.rb | 20 +- lib/ld-eventsource/errors.rb | 2 +- lib/ld-eventsource/impl/backoff.rb | 4 +- lib/ld-eventsource/impl/event_parser.rb | 8 +- spec/backoff_spec.rb | 2 +- spec/buffered_line_reader_spec.rb | 12 +- spec/client_spec.rb | 38 +- spec/event_parser_spec.rb | 60 +- spec/http_stub.rb | 2 +- 20 files changed, 1042 insertions(+), 132 deletions(-) create mode 100644 .rubocop.yml diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 4293ff7..94ab372 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -1,8 +1,11 @@ name: CI Workflow -description: 'Shared CI workflow.' +description: "Shared CI workflow." inputs: ruby-version: - description: 'The version of ruby to setup and run' + description: "The version of ruby to setup and run" + required: true + token: + description: "GH token used to fetch the SDK test harness" required: true runs: @@ -11,11 +14,10 @@ runs: - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ inputs.ruby-version }} - bundler: 2.2.10 - name: Install dependencies shell: bash - run: bundle _2.2.10_ install + run: bundle install - name: Skip end to end tests for jruby if: ${{ startsWith(inputs.ruby-version, 'jruby') }} @@ -24,9 +26,28 @@ runs: - name: Run tests shell: bash - run: bundle _2.2.10_ exec rspec spec + run: bundle exec rspec spec + + - name: Run RuboCop + if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} + shell: bash + run: bundle exec rubocop --parallel + + - name: Build contract tests + if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} + shell: bash + run: make build-contract-tests - - name: Run contract tests + - name: Start contract test service if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} shell: bash - run: make contract-tests + run: make start-contract-test-service-bg + + - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.1.0 + if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} + with: + test_service_port: 8000 + token: ${{ inputs.token }} + repo: sse-contract-tests + branch: main + version: v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d33d58..80bf00a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,13 @@ name: Run CI on: push: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' # Do not need to run CI for markdown changes. + - "**.md" # Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" jobs: build: @@ -17,14 +17,11 @@ jobs: fail-fast: false matrix: ruby-version: - - '2.5' - - '2.6' - - '2.7' - - '3.0' - - '3.1' - - '3.2' - - jruby-9.3 + - "3.2" + - "3.3" + - "3.4" - jruby-9.4 + - jruby-10.0 steps: - uses: actions/checkout@v4 @@ -34,3 +31,4 @@ jobs: - uses: ./.github/actions/ci with: ruby-version: ${{ matrix.ruby-version }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 770d4c5..9adcc89 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: inputs: dry_run: - description: 'Is this a dry run. If so no package will be published.' + description: "Is this a dry run. If so no package will be published." type: boolean required: true @@ -18,16 +18,17 @@ jobs: - uses: actions/checkout@v4 - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 - name: 'Get rubygems API key' + name: "Get rubygems API key" with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} - ssm_parameter_pairs: '/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY' + ssm_parameter_pairs: "/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY" - id: build-and-test name: Build and Test uses: ./.github/actions/ci with: - ruby-version: '3.0' + ruby-version: "3.2" + token: ${{ secrets.GITHUB_TOKEN }} - id: publish name: Publish Package diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index d55f8fa..8aec81b 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -23,15 +23,16 @@ jobs: - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 if: ${{ steps.release.outputs.releases_created == 'true' }} - name: 'Get rubygems API key' + name: "Get rubygems API key" with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} - ssm_parameter_pairs: '/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY' + ssm_parameter_pairs: "/production/common/releasing/rubygems/api_key = GEM_HOST_API_KEY" - uses: ./.github/actions/ci if: ${{ steps.release.outputs.releases_created == 'true' }} with: - ruby-version: '3.0' + ruby-version: "3.2" + token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/publish if: ${{ steps.release.outputs.releases_created == 'true' }} diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..43d65d5 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,889 @@ +require: + - rubocop-performance + +AllCops: + TargetRubyVersion: 3.1 + Include: + - lib/**/*.rb + - spec/**/*.rb + - contract-tests/**/*.rb + NewCops: disable + +Naming/AccessorMethodName: + Description: Check the naming of accessor methods for get_/set_. + Enabled: false + +Style/AccessModifierDeclarations: + Description: "Access modifiers should be declared to apply to a group of methods or inline before each method, depending on configuration." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#alias-method" + Enabled: false + +Style/Alias: + Description: "Use alias_method instead of alias." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#alias-method" + Enabled: false + +Style/ArrayJoin: + Description: "Use Array#join instead of Array#*." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#array-join" + Enabled: false + +Style/AsciiComments: + Description: "Use only ascii symbols in comments." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#english-comments" + Enabled: false + +Naming/AsciiIdentifiers: + Description: "Use only ascii symbols in identifiers." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#english-identifiers" + Enabled: false + +Naming/VariableName: + Description: "Makes sure that all variables use the configured style, snake_case or camelCase, for their names." + Enabled: false + +Style/Attr: + Description: "Checks for uses of Module#attr." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#attr" + Enabled: false + +Metrics/AbcSize: + Description: "Checks that the ABC size of methods is not higher than the configured maximum." + Enabled: false + +Metrics/BlockLength: + Description: "Checks if the length of a block exceeds some maximum value." + Enabled: false + +Metrics/BlockNesting: + Description: "Avoid excessive block nesting" + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count" + Enabled: false + +Style/CaseEquality: + Description: "Avoid explicit use of the case equality operator(===)." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-case-equality" + Enabled: false + +Style/CharacterLiteral: + Description: "Checks for uses of character literals." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-character-literals" + Enabled: false + +Style/ClassAndModuleChildren: + Description: "Checks style of children classes and modules." + Enabled: true + EnforcedStyle: nested + +Metrics/ClassLength: + Description: "Avoid classes longer than 100 lines of code." + Enabled: false + +Metrics/ModuleLength: + Description: "Avoid modules longer than 100 lines of code." + Enabled: false + +Style/ClassVars: + Description: "Avoid the use of class variables." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-class-vars" + Enabled: false + +Style/CollectionMethods: + Enabled: true + PreferredMethods: + find: detect + inject: reduce + collect: map + find_all: select + +Style/ColonMethodCall: + Description: "Do not use :: for method call." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#double-colons" + Enabled: false + +Style/CommentAnnotation: + Description: >- + Checks formatting of special comments + (TODO, FIXME, OPTIMIZE, HACK, REVIEW). + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#annotate-keywords" + Enabled: false + +Metrics/CyclomaticComplexity: + Description: >- + A complexity metric that is strongly correlated to the number + of test cases needed to validate a method. + Enabled: false + +Style/PreferredHashMethods: + Description: "Checks for use of deprecated Hash methods." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#hash-key" + Enabled: false + +Style/Documentation: + Description: "Document classes and non-namespace modules." + Enabled: false + +Style/DoubleNegation: + Description: "Checks for uses of double negation (!!)." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-bang-bang" + Enabled: false + +Style/EachWithObject: + Description: "Prefer `each_with_object` over `inject` or `reduce`." + Enabled: false + +Style/EmptyLiteral: + Description: "Prefer literals to Array.new/Hash.new/String.new." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#literal-array-hash" + Enabled: false + +# Checks whether the source file has a utf-8 encoding comment or not +# AutoCorrectEncodingComment must match the regex +# /#.*coding\s?[:=]\s?(?:UTF|utf)-8/ +Style/Encoding: + Enabled: false + +Style/EvenOdd: + Description: "Favor the use of Fixnum#even? && Fixnum#odd?" + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#predicate-methods" + Enabled: false + +Naming/FileName: + Description: "Use snake_case for source file names." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#snake-case-files" + Enabled: false + +Lint/FlipFlop: + Description: "Checks for flip flops" + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-flip-flops" + Enabled: false + +Style/FrozenStringLiteralComment: + Description: "Helps you transition from mutable string literals to frozen string literals." + Enabled: false + +Style/FormatString: + Description: "Enforce the use of Kernel#sprintf, Kernel#format or String#%." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#sprintf" + Enabled: false + +Style/GlobalVars: + Description: "Do not introduce global variables." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#instance-vars" + Reference: "https://www.zenspider.com/ruby/quickref.html" + Enabled: false + +Style/GuardClause: + Description: "Check for conditionals that can be replaced with guard clauses" + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals" + Enabled: false + +Style/IfUnlessModifier: + Description: >- + Favor modifier if/unless usage when you have a + single-line body. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier" + Enabled: false + +Style/IfWithSemicolon: + Description: "Do not use if x; .... Use the ternary operator instead." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs" + Enabled: false + +Style/InlineComment: + Description: "Avoid inline comments." + Enabled: false + +Style/Lambda: + Description: "Use the new lambda literal syntax for single-line blocks." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#lambda-multi-line" + Enabled: false + +Style/LambdaCall: + Description: "Use lambda.call(...) instead of lambda.(...)." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#proc-call" + Enabled: false + +Style/LineEndConcatenation: + Description: >- + Use \ instead of + or << to concatenate two string literals at + line end. + Enabled: false + +Layout/LineLength: + Description: "Limit lines to 150 characters." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#80-character-limits" + Max: 180 + +Metrics/MethodLength: + Description: "Avoid methods longer than 10 lines of code." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#short-methods" + Enabled: false + +Style/ModuleFunction: + Description: "Checks for usage of `extend self` in modules." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#module-function" + Enabled: false + +Style/NegatedIf: + Description: >- + Favor unless over if for negative conditions + (or control flow or). + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#unless-for-negatives" + Enabled: true + +Style/NegatedWhile: + Description: "Favor until over while for negative conditions." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#until-for-negatives" + Enabled: true + +Style/Next: + Description: "Use `next` to skip iteration instead of a condition at the end." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals" + Enabled: false + +Style/NilComparison: + Description: "Prefer x.nil? to x == nil." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#predicate-methods" + Enabled: false + +Style/Not: + Description: "Use ! instead of not." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#bang-not-not" + Enabled: false + +Style/NumericLiterals: + Description: >- + Add underscores to large numeric literals to improve their + readability. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics" + Enabled: false + +Style/OneLineConditional: + Description: >- + Favor the ternary operator(?:) over + if/then/else/end constructs. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#ternary-operator" + Enabled: false + +Naming/BinaryOperatorParameterName: + Description: "When defining binary operators, name the argument other." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#other-arg" + Enabled: false + +Metrics/ParameterLists: + Description: "Avoid parameter lists longer than three or four parameters." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#too-many-params" + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Style/PercentLiteralDelimiters: + Description: "Use `%`-literal delimiters consistently" + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#percent-literal-braces" + Enabled: false + +Style/PerlBackrefs: + Description: "Avoid Perl-style regex back references." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers" + Enabled: false + +Naming/PredicateName: + Description: "Check the names of predicate methods." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark" + ForbiddenPrefixes: + - is_ + Exclude: + - spec/**/* + +Style/Proc: + Description: "Use proc instead of Proc.new." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#proc" + Enabled: false + +Style/RaiseArgs: + Description: "Checks the arguments passed to raise/fail." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#exception-class-messages" + Enabled: false + +Style/RegexpLiteral: + Description: "Use / or %r around regular expressions." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#percent-r" + Enabled: false + +Style/SelfAssignment: + Description: >- + Checks for places where self-assignment shorthand should have + been used. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#self-assignment" + Enabled: false + +Style/SingleLineBlockParams: + Description: "Enforces the names of some block params." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#reduce-blocks" + Enabled: false + +Style/SingleLineMethods: + Description: "Avoid single-line methods." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-single-line-methods" + Enabled: false + +Style/SignalException: + Description: "Checks for proper usage of fail and raise." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#fail-method" + Enabled: false + +Style/SpecialGlobalVars: + Description: "Avoid Perl-style global variables." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms" + Enabled: false + +Style/StringLiterals: + Description: "Checks if uses of quotes match the configured preference." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#consistent-string-literals" + EnforcedStyle: double_quotes + Enabled: false + +Style/TrailingCommaInArguments: + Description: "Checks for trailing comma in argument lists." + StyleGuide: "#no-trailing-params-comma" + Enabled: true + +Style/TrailingCommaInArrayLiteral: + Description: "Checks for trailing comma in array and hash literals." + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + Description: "Checks for trailing comma in array and hash literals." + EnforcedStyleForMultiline: comma + +Style/TrivialAccessors: + Description: "Prefer attr_* methods to trivial readers/writers." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#attr_family" + Enabled: false + +Style/VariableInterpolation: + Description: >- + Don't interpolate global, instance and class variables + directly in strings. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#curlies-interpolate" + Enabled: false + +Style/WhenThen: + Description: "Use when x then ... for one-line cases." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#one-line-cases" + Enabled: false + +Style/WhileUntilModifier: + Description: >- + Favor modifier while/until usage when you have a + single-line body. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier" + Enabled: false + +Style/WordArray: + Description: "Use %w or %W for arrays of words." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#percent-w" + Enabled: false + +# Layout +Layout/DotPosition: + Description: "Checks the position of the dot in multi-line method calls." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains" + EnforcedStyle: leading + +Layout/ExtraSpacing: + Description: "Do not use unnecessary spacing." + Enabled: true + AllowBeforeTrailingComments: true + +Layout/MultilineOperationIndentation: + Description: >- + Checks indentation of binary operations that span more than + one line. + Enabled: true + EnforcedStyle: indented + +Layout/InitialIndentation: + Description: >- + Checks the indentation of the first non-blank non-comment line in a file. + Enabled: false + +Layout/SpaceInsideArrayLiteralBrackets: + Description: "Checks that brackets used for array literals have or don't have surrounding space depending on configuration." + Enabled: false + +Layout/TrailingWhitespace: + Description: "Ensures all trailing whitespace has been removed" + Enabled: true + +# Lint + +Lint/AmbiguousOperator: + Description: >- + Checks for ambiguous operators in the first argument of a + method invocation without parentheses. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#parens-as-args" + Enabled: false + +Lint/AmbiguousRegexpLiteral: + Description: >- + Checks for ambiguous regexp literals in the first argument of + a method invocation without parenthesis. + Enabled: false + +Lint/AssignmentInCondition: + Description: "Don't use assignment in conditions." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition" + Enabled: false + +Lint/CircularArgumentReference: + Description: "Don't refer to the keyword argument in the default value." + Enabled: false + +Layout/ConditionPosition: + Description: >- + Checks for condition placed in a confusing position relative to + the keyword. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#same-line-condition" + Enabled: false + +Lint/DeprecatedClassMethods: + Description: "Check for deprecated class method calls." + Enabled: false + +Lint/DuplicateHashKey: + Description: "Check for duplicate keys in hash literals." + Enabled: false + +Lint/EachWithObjectArgument: + Description: "Check for immutable argument given to each_with_object." + Enabled: false + +Lint/ElseLayout: + Description: "Check for odd code arrangement in an else block." + Enabled: false + +Lint/FormatParameterMismatch: + Description: "The number of parameters to format/sprint must match the fields." + Enabled: false + +Lint/SuppressedException: + Description: "Don't suppress exception." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions" + Enabled: false + +Lint/LiteralAsCondition: + Description: "Checks of literals used in conditions." + Enabled: false + +Lint/LiteralInInterpolation: + Description: "Checks for literals used in interpolation." + Enabled: false + +Lint/Loop: + Description: >- + Use Kernel#loop with break rather than begin/end/until or + begin/end/while for post-loop tests. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#loop-with-break" + Enabled: false + +Lint/NestedMethodDefinition: + Description: "Do not use nested method definitions." + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-nested-methods" + Enabled: false + +Lint/NonLocalExitFromIterator: + Description: "Do not use return in iterator to cause non-local exit." + Enabled: false + +Lint/ParenthesesAsGroupedExpression: + Description: >- + Checks for method calls with a space before the opening + parenthesis. + StyleGuide: "https://github.com/bbatsov/ruby-style-guide#parens-no-spaces" + Enabled: false + +Lint/RequireParentheses: + Description: >- + Use parentheses in the method call to avoid confusion + about precedence. + Enabled: false + +Lint/UnderscorePrefixedVariableName: + Description: "Do not use prefix `_` for a variable that is used." + Enabled: false + +Lint/RedundantCopDisableDirective: + Description: >- + Checks for rubocop:disable comments that can be removed. + Note: this cop is not disabled when disabling all cops. + It must be explicitly disabled. + Enabled: false + +Lint/Void: + Description: "Possible use of operator/literal/variable in void context." + Enabled: false + +# Performance + +Performance/CaseWhenSplat: + Description: >- + Place `when` conditions that use splat at the end + of the list of `when` branches. + Enabled: false + +Performance/Count: + Description: >- + Use `count` instead of `select...size`, `reject...size`, + `select...count`, `reject...count`, `select...length`, + and `reject...length`. + Enabled: false + +Performance/Detect: + Description: >- + Use `detect` instead of `select.first`, `find_all.first`, + `select.last`, and `find_all.last`. + Reference: "https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code" + Enabled: false + +Performance/FlatMap: + Description: >- + Use `Enumerable#flat_map` + instead of `Enumerable#map...Array#flatten(1)` + or `Enumberable#collect..Array#flatten(1)` + Reference: "https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code" + Enabled: false + +Performance/ReverseEach: + Description: "Use `reverse_each` instead of `reverse.each`." + Reference: "https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code" + Enabled: false + +Style/Sample: + Description: >- + Use `sample` instead of `shuffle.first`, + `shuffle.last`, and `shuffle[Fixnum]`. + Reference: "https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code" + Enabled: false + +Performance/Size: + Description: >- + Use `size` instead of `count` for counting + the number of elements in `Array` and `Hash`. + Reference: "https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code" + Enabled: false + +Performance/StringReplacement: + Description: >- + Use `tr` instead of `gsub` when you are replacing the same + number of characters. Use `delete` instead of `gsub` when + you are deleting characters. + Reference: "https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code" + Enabled: false + +# Disabled temporarily while we bring code base inline +Layout/ArgumentAlignment: + Enabled: false + +Layout/ArrayAlignment: + Enabled: false + +Layout/BlockEndNewline: + Enabled: false + +Layout/CaseIndentation: + Enabled: false + +Layout/ClosingHeredocIndentation: + Enabled: false + +Layout/ClosingParenthesisIndentation: + Enabled: false + +Layout/CommentIndentation: + Enabled: false + +Layout/ElseAlignment: + Enabled: false + +Layout/EmptyLineAfterGuardClause: + Enabled: false + +Layout/EmptyLineBetweenDefs: + Enabled: false + +Layout/EmptyLines: + Enabled: false + +Layout/EmptyLinesAroundBlockBody: + Enabled: false + +Layout/EmptyLinesAroundMethodBody: + Enabled: false + +Layout/EmptyLinesAroundModuleBody: + Enabled: false + +Layout/EndAlignment: + Enabled: false + +Layout/FirstArgumentIndentation: + Enabled: false + +Layout/FirstHashElementIndentation: + Enabled: false + +Layout/HashAlignment: + Enabled: false + +Layout/HeredocIndentation: + Enabled: false + +Layout/IndentationWidth: + Enabled: false + +Layout/LeadingCommentSpace: + Enabled: false + +Layout/LeadingEmptyLines: + Enabled: false + +Layout/MultilineArrayBraceLayout: + Enabled: false + +Layout/MultilineBlockLayout: + Enabled: false + +Layout/MultilineHashBraceLayout: + Enabled: false + +Layout/MultilineMethodCallBraceLayout: + Enabled: false + +Layout/MultilineMethodCallIndentation: + Enabled: false + +Layout/ParameterAlignment: + Enabled: false + +Layout/SpaceAfterComma: + Enabled: false + +Layout/SpaceAroundBlockParameters: + Enabled: false + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: false + +Layout/SpaceAroundOperators: + Enabled: false + +Layout/SpaceBeforeBlockBraces: + Enabled: false + +Layout/SpaceBeforeComma: + Enabled: false + +Layout/SpaceInsideBlockBraces: + Enabled: false + +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +Layout/SpaceInsideReferenceBrackets: + Enabled: false + +Layout/TrailingEmptyLines: + Enabled: false + +Lint/ConstantDefinitionInBlock: + Enabled: false + +Lint/IneffectiveAccessModifier: + Enabled: false + +Lint/MissingCopEnableDirective: + Enabled: false + +Lint/RedundantRequireStatement: + Enabled: false + +Lint/StructNewOverride: + Enabled: false + +Lint/UnusedBlockArgument: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false + +Lint/UselessAccessModifier: + Enabled: false + +Lint/UselessAssignment: + Enabled: false + +Lint/UselessMethodDefinition: + Enabled: false + +Naming/BlockParameterName: + Enabled: false + +Naming/HeredocDelimiterNaming: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Naming/RescuedExceptionsVariableName: + Enabled: false + +Naming/VariableNumber: + Enabled: false + +Style/AccessorGrouping: + Enabled: false + +Style/AndOr: + Enabled: false + +Style/BlockDelimiters: + Enabled: false + +Style/CaseLikeIf: + Enabled: false + +Style/CombinableLoops: + Enabled: false + +Style/CommentedKeyword: + Enabled: false + +Style/ConditionalAssignment: + Enabled: false + +Style/DefWithParentheses: + Enabled: false + +Style/EmptyElse: + Enabled: false + +Style/EmptyMethod: + Enabled: false + +Style/ExplicitBlockArgument: + Enabled: false + +Style/For: + Enabled: false + +Style/FormatStringToken: + Enabled: false + +Style/GlobalStdStream: + Enabled: false + +Style/HashEachMethods: + Enabled: false + +Style/HashSyntax: + Enabled: false + +Style/InfiniteLoop: + Enabled: false + +Style/InverseMethods: + Enabled: false + +Style/MethodCallWithoutArgsParentheses: + Enabled: false + +Style/MissingRespondToMissing: + Enabled: false + +Style/MultilineIfThen: + Enabled: false + +Style/MultilineTernaryOperator: + Enabled: false + +Style/MultipleComparison: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Style/OptionalBooleanParameter: + Enabled: false + +Style/ParallelAssignment: + Enabled: false + +Style/RedundantAssignment: + Enabled: false + +Style/RedundantBegin: + Enabled: false + +Style/RedundantCondition: + Enabled: true + +Style/RedundantException: + Enabled: false + +Style/RedundantFileExtensionInRequire: + Enabled: false + +Style/RedundantParentheses: + Enabled: true + +Style/RedundantRegexpEscape: + Enabled: false + +Style/RedundantReturn: + Enabled: true + +Style/RedundantSelf: + Enabled: false + +Style/RescueStandardError: + Enabled: false + +Style/SafeNavigation: + Enabled: false + +Style/Semicolon: + Enabled: true + AllowAsExpressionSeparator: true + +Style/SlicingWithRange: + Enabled: false + +Style/SoleNestedConditional: + Enabled: false + +Style/StringConcatenation: + Enabled: false + +Style/SymbolArray: + Enabled: false + +Style/SymbolProc: + Enabled: false + +Style/TernaryParentheses: + Enabled: false + +Style/TrailingUnderscoreVariable: + Enabled: false + +Style/WhileUntilDo: + Enabled: false + +Style/ZeroLengthPredicate: + Enabled: false diff --git a/Makefile b/Makefile index 34a27c6..27c23d0 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log build-contract-tests: - @cd contract-tests && bundle _2.2.10_ install + @cd contract-tests && bundle install start-contract-test-service: - @cd contract-tests && bundle _2.2.10_ exec ruby service.rb + @cd contract-tests && bundle exec ruby service.rb start-contract-test-service-bg: @echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" diff --git a/README.md b/README.md index 2b6e13c..f360d15 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Parts of this code are based on https://github.com/Tonkpils/celluloid-eventsourc Supported Ruby versions ----------------------- -This gem has a minimum Ruby version of 2.5, or 9.2 for JRuby. +This gem has a minimum Ruby version of 3.1. Quick setup ----------- diff --git a/contract-tests/Gemfile b/contract-tests/Gemfile index 84bdd56..7e4c662 100644 --- a/contract-tests/Gemfile +++ b/contract-tests/Gemfile @@ -2,9 +2,7 @@ source 'https://rubygems.org' gem 'ld-eventsource', path: '..' -gem 'sinatra', '~> 2.1' -# Sinatra can work with several server frameworks. In JRuby, we have to use glassfish (which -# is only available in JRuby). Otherwise we use thin (which is not available in JRuby). -gem 'glassfish', :platforms => :jruby -gem 'thin', :platforms => :ruby gem 'json' +gem 'puma', '~> 6.6' +gem 'rackup', '~> 2.2' +gem 'sinatra', '>= 4.1' diff --git a/contract-tests/service.rb b/contract-tests/service.rb index 839822c..0a1d303 100644 --- a/contract-tests/service.rb +++ b/contract-tests/service.rb @@ -22,8 +22,8 @@ capabilities: [ 'headers', 'last-event-id', - 'read-timeout' - ] + 'read-timeout', + ], }.to_json end @@ -58,8 +58,8 @@ last_event_id: opts[:lastEventId], read_timeout: opts[:readTimeoutMs].nil? ? nil : (opts[:readTimeoutMs].to_f / 1000), reconnect_time: opts[:initialDelayMs].nil? ? nil : (opts[:initialDelayMs].to_f / 1000) - ) do |sse| - entity = StreamEntity.new(sse, tag, callbackUrl) + ) do |client| + entity = StreamEntity.new(client, tag, callbackUrl) end streams[streamId] = entity diff --git a/contract-tests/stream_entity.rb b/contract-tests/stream_entity.rb index 0c62543..078f95a 100644 --- a/contract-tests/stream_entity.rb +++ b/contract-tests/stream_entity.rb @@ -23,8 +23,8 @@ def on_event(event) event: { type: event.type, data: event.data, - id: event.last_event_id - } + id: event.last_event_id, + }, } self.send_message(message) end @@ -33,7 +33,7 @@ def on_error(error) $log.info("#{@tag} Received error from stream: #{error}") message = { kind: 'error', - error: error + error: error, } self.send_message(message) end diff --git a/ld-eventsource.gemspec b/ld-eventsource.gemspec index d671bc4..de59b25 100644 --- a/ld-eventsource.gemspec +++ b/ld-eventsource.gemspec @@ -1,29 +1,31 @@ # coding: utf-8 -lib = File.expand_path("../lib", __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "ld-eventsource/version" +require 'ld-eventsource/version' -# rubocop:disable Metrics/BlockLength Gem::Specification.new do |spec| - spec.name = "ld-eventsource" + spec.name = 'ld-eventsource' spec.version = SSE::VERSION - spec.authors = ["LaunchDarkly"] - spec.email = ["team@launchdarkly.com"] - spec.summary = "LaunchDarkly SSE client" - spec.description = "LaunchDarkly SSE client for Ruby" - spec.homepage = "https://github.com/launchdarkly/ruby-eventsource" - spec.license = "Apache-2.0" + spec.authors = ['LaunchDarkly'] + spec.email = ['team@launchdarkly.com'] + spec.summary = 'LaunchDarkly SSE client' + spec.description = 'LaunchDarkly SSE client for Ruby' + spec.homepage = 'https://github.com/launchdarkly/ruby-eventsource' + spec.license = 'Apache-2.0' - spec.files = Dir.glob("lib/**/*") + ["README.md", "LICENSE"] + + spec.files = Dir.glob('lib/**/*') + ['README.md', 'LICENSE'] spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 3.1' - spec.add_development_dependency "bundler", "2.2.10" - spec.add_development_dependency "rspec", "~> 3.2" - spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0" - spec.add_development_dependency "webrick", "~> 1.7" + spec.add_development_dependency 'rspec', '~> 3.2' + spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0' + spec.add_development_dependency "rubocop", "~> 1.37" + spec.add_development_dependency "rubocop-performance", "~> 1.15" + spec.add_development_dependency 'webrick', '~> 1.7' - spec.add_runtime_dependency "concurrent-ruby", "~> 1.0" - spec.add_runtime_dependency "http", ">= 4.4.1", "< 6.0.0" + spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0' + spec.add_runtime_dependency 'http', '>= 4.4.1', '< 6.0.0' end diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index 6355a62..c018151 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -120,14 +120,14 @@ def initialize(uri, if @proxy http_client_options["proxy"] = { :proxy_address => @proxy.host, - :proxy_port => @proxy.port + :proxy_port => @proxy.port, } end @http_client = HTTP::Client.new(http_client_options) .timeout({ read: read_timeout, - connect: connect_timeout + connect: connect_timeout, }) @cxn = nil @lock = Mutex.new @@ -202,13 +202,13 @@ def closed? private def reset_http - @http_client.close if !@http_client.nil? + @http_client.close unless @http_client.nil? close_connection end def close_connection @lock.synchronize do - @cxn.connection.close if !@cxn.nil? + @cxn.connection.close unless @cxn.nil? @cxn = nil end end @@ -216,12 +216,12 @@ def close_connection def default_logger log = ::Logger.new($stdout) log.level = ::Logger::WARN - log.progname = 'ld-eventsource' + log.progname = 'ld-eventsource' log end def run_stream - while !@stopped.value + until @stopped.value close_connection begin resp = connect @@ -231,7 +231,7 @@ def run_stream # There's a potential race if close was called in the middle of the previous line, i.e. after we # connected but before @cxn was set. Checking the variable again is a bit clunky but avoids that. return if @stopped.value - read_stream(resp) if !resp.nil? + read_stream(resp) unless resp.nil? rescue => e # When we deliberately close the connection, it will usually trigger an exception. The exact type # of exception depends on the specific Ruby runtime. But @stopped will always be set in this case. @@ -263,7 +263,7 @@ def connect begin @logger.info { "Connecting to event stream at #{@uri}" } cxn = @http_client.request("GET", @uri, { - headers: build_headers + headers: build_headers, }) if cxn.status.code == 200 content_type = cxn.content_type.mime_type @@ -332,7 +332,7 @@ def read_stream(cxn) def dispatch_event(event) @logger.debug { "Received event: #{event}" } - @last_id = event.id if !event.id.nil? + @last_id = event.id unless event.id.nil? # Pass the event to the caller @on[:event].call(event) @@ -353,7 +353,7 @@ def build_headers h = { 'Accept' => 'text/event-stream', 'Cache-Control' => 'no-cache', - 'User-Agent' => 'ruby-eventsource' + 'User-Agent' => 'ruby-eventsource', } h['Last-Event-Id'] = @last_id if !@last_id.nil? && @last_id != "" h.merge(@headers) diff --git a/lib/ld-eventsource/errors.rb b/lib/ld-eventsource/errors.rb index 6625642..6fe05ba 100644 --- a/lib/ld-eventsource/errors.rb +++ b/lib/ld-eventsource/errors.rb @@ -30,7 +30,7 @@ def initialize(status, message) # class HTTPContentTypeError < StandardError def initialize(type) - @content_type = type + @content_type = type super("invalid content type \"#{type}\"") end diff --git a/lib/ld-eventsource/impl/backoff.rb b/lib/ld-eventsource/impl/backoff.rb index 543da63..a540bf3 100644 --- a/lib/ld-eventsource/impl/backoff.rb +++ b/lib/ld-eventsource/impl/backoff.rb @@ -34,12 +34,12 @@ def initialize(base_interval, max_interval, reconnect_reset_interval: 60) # @return [Float] the next interval in seconds # def next_interval - if !@last_good_time.nil? + unless @last_good_time.nil? good_duration = Time.now.to_f - @last_good_time @attempts = 0 if good_duration >= @reconnect_reset_interval end @last_good_time = nil - target = ([@base_interval * (2 ** @attempts), @max_interval].min).to_f + target = [@base_interval * (2 ** @attempts), @max_interval].min.to_f @attempts += 1 if target == 0 0 # in some Ruby versions it's illegal to call rand(0) diff --git a/lib/ld-eventsource/impl/event_parser.rb b/lib/ld-eventsource/impl/event_parser.rb index bb6c8e4..b766aeb 100644 --- a/lib/ld-eventsource/impl/event_parser.rb +++ b/lib/ld-eventsource/impl/event_parser.rb @@ -36,7 +36,7 @@ def items if line.empty? event = maybe_create_event reset_buffers - gen.yield event if !event.nil? + gen.yield event unless event.nil? elsif (pos = line.index(':')) name = line.slice(0...pos) @@ -45,7 +45,7 @@ def items line = line.slice(pos..-1) item = process_field(name, line) - gen.yield item if !item.nil? + gen.yield item unless item.nil? end end end @@ -72,7 +72,7 @@ def process_field(name, value) end @have_data = true when "id" - if !value.include?("\x00") + unless value.include?("\x00") @id = value @last_event_id = value end @@ -85,7 +85,7 @@ def process_field(name, value) end def maybe_create_event - return nil if !@have_data + return nil unless @have_data StreamEvent.new(@type || :message, @data, @id, @last_event_id) end end diff --git a/spec/backoff_spec.rb b/spec/backoff_spec.rb index 6a1f9af..e3b81da 100644 --- a/spec/backoff_spec.rb +++ b/spec/backoff_spec.rb @@ -52,7 +52,7 @@ module Impl initial = 0 max = 60 b = Backoff.new(initial, max) - + for i in 1..6 do interval = b.next_interval expect(interval).to eq(0) diff --git a/spec/buffered_line_reader_spec.rb b/spec/buffered_line_reader_spec.rb index 376a1fa..a57c7eb 100644 --- a/spec/buffered_line_reader_spec.rb +++ b/spec/buffered_line_reader_spec.rb @@ -4,7 +4,7 @@ def make_tests(name, input_line_chunks:, expected_lines:) [{ name: "#{name}: one chunk per line", chunks: input_line_chunks, - expected: expected_lines + expected: expected_lines, }].concat( (1..4).map do |size| # Here we're lumping together all the content into one string and then @@ -13,11 +13,11 @@ def make_tests(name, input_line_chunks:, expected_lines:) # chunks would be ["ab", "cd", "\ne", "fg", "\n"]. This helps to find edge # case problems related to line terminators falling at the start of a chunk # or in the middle, etc. - ({ + { name: "#{name}: #{size}-character chunks", chunks: input_line_chunks.join().chars.each_slice(size).map { |a| a.join }, - expected: expected_lines - }) + expected: expected_lines, + } end ) end @@ -43,7 +43,7 @@ def tests_for_terminator(term, desc) make_tests("multi-line chunks", input_line_chunks: ["first line" + term + "second line" + term + "third", " line" + term + "fourth line" + term], - expected_lines: ["first line", "second line", "third line", "fourth line"]) + expected_lines: ["first line", "second line", "third line", "fourth line"]), ].flatten end @@ -53,7 +53,7 @@ def tests_for_terminator(term, desc) terminators = { "CR": "\r", "LF": "\n", - "CRLF": "\r\n" + "CRLF": "\r\n", } terminators.each do |desc, term| diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 51f516d..ea7b849 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -6,7 +6,7 @@ # describe SSE::Client do before(:each) do - skip("end-to-end HTTP tests are disabled because they're unreliable on this platform") if !stub_http_server_available? + skip("end-to-end HTTP tests are disabled because they're unreliable on this platform") unless stub_http_server_available? end subject { SSE::Client } @@ -57,7 +57,7 @@ def send_stream_content(res, content, keep_open:) requests << req send_stream_content(res, "", keep_open: true) end - + headers = { "Authorization" => "secret" } with_client(subject.new(server.base_uri, headers: headers)) do |client| @@ -68,7 +68,7 @@ def send_stream_content(res, content, keep_open:) "host" => ["127.0.0.1:" + server.port.to_s], "authorization" => ["secret"], "user-agent" => ["ruby-eventsource"], - "connection" => ["close"] + "connection" => ["close"], }) end end @@ -82,7 +82,7 @@ def send_stream_content(res, content, keep_open:) requests << req send_stream_content(res, "", keep_open: true) end - + headers = { "Authorization" => "secret" } with_client(subject.new(server.base_uri, headers: headers, last_event_id: id)) do |client| @@ -94,7 +94,7 @@ def send_stream_content(res, content, keep_open:) "authorization" => ["secret"], "last-event-id" => [id], "user-agent" => ["ruby-eventsource"], - "connection" => ["close"] + "connection" => ["close"], }) end end @@ -112,7 +112,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(event_sink.pop).to eq(simple_event_2) end @@ -133,9 +133,9 @@ def send_stream_content(res, content, keep_open:) c.on_error { |error| error_sink << error } end - with_client(client) do |client| + with_client(client) do |c| event_sink.pop # wait till we have definitely started reading the stream - client.close + c.close sleep 0.25 # there's no way to really know when the stream thread has finished expect(error_sink.empty?).to be true end @@ -164,7 +164,7 @@ def send_stream_content(res, content, keep_open:) c.on_error { |error| error_sink << error } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(error_sink.pop).to eq(SSE::Errors::HTTPStatusError.new(500, "sorry")) expect(attempt).to eq 2 @@ -195,7 +195,7 @@ def send_stream_content(res, content, keep_open:) c.on_error { |error| error_sink << error } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(error_sink.pop).to eq(SSE::Errors::HTTPContentTypeError.new("text/plain")) expect(attempt).to eq 2 @@ -220,7 +220,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(attempt).to eq 2 end @@ -241,7 +241,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(event_sink.pop).to eq(simple_event_2) expect(attempt).to eq 2 @@ -268,7 +268,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| req1 = requests.pop req2 = requests.pop expect(req2.header["last-event-id"]).to eq([ "a" ]) @@ -294,7 +294,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| last_interval = nil max_requests.times do |i| expect(event_sink.pop).to eq(simple_event_1) @@ -334,7 +334,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| last_interval = nil max_requests.times do |i| expect(event_sink.pop).to eq(simple_event_1) @@ -369,7 +369,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) interval = request_times[1] - request_times[0] expect(interval).to be < 0.5 @@ -389,7 +389,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| expect(event_sink.pop).to eq(simple_event_1) expect(proxy.request_count).to eq(1) end @@ -424,7 +424,7 @@ def send_stream_content(res, content, keep_open:) c.on_event { |event| event_sink << event } end - with_client(client) do |client| + with_client(client) do |c| 4.times { expect(event_sink.pop).to eq(simple_event_1) } @@ -438,7 +438,7 @@ def send_stream_content(res, content, keep_open:) server.setup_response("/") do |req,res| send_stream_content(res, "", keep_open: true) end - + with_client(subject.new(server.base_uri)) do |client| expect(client.closed?).to be(false) diff --git a/spec/event_parser_spec.rb b/spec/event_parser_spec.rb index 264d73d..c77609e 100644 --- a/spec/event_parser_spec.rb +++ b/spec/event_parser_spec.rb @@ -12,10 +12,10 @@ def verify_parsed_events(lines:, expected_events:) it "parses an event with only data" do lines = [ "data: def", - "" + "", ] ep = subject.new(lines) - + expected_event = SSE::StreamEvent.new(:message, "def", nil) output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -26,10 +26,10 @@ def verify_parsed_events(lines:, expected_events:) "event: abc", "data: def", "id: 1", - "" + "", ] ep = subject.new(lines) - + expected_event = SSE::StreamEvent.new(:abc, "def", "1", "1") output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -39,10 +39,10 @@ def verify_parsed_events(lines:, expected_events:) lines = [ "event: abc", "data: def", - "" + "", ] ep = subject.new(lines, "1") - + expected_event = SSE::StreamEvent.new(:abc, "def", nil, "1") output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -53,10 +53,10 @@ def verify_parsed_events(lines:, expected_events:) "event: abc", "data: def", "id:", - "" + "", ] ep = subject.new(lines, "1") - + expected_event = SSE::StreamEvent.new(:abc, "def", "", "") output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -68,23 +68,23 @@ def verify_parsed_events(lines:, expected_events:) "event: abc", "data: def", "id: 12\x0034", - "" + "", ] ep = subject.new(lines, "1") - + expected_event = SSE::StreamEvent.new(:abc, "def", nil, "1") output = ep.items.to_a expect(output).to eq([ expected_event ]) end - + it "parses an event with multi-line data" do lines = [ "data: def", "data: ghi", - "" + "", ] ep = subject.new(lines) - + expected_event = SSE::StreamEvent.new(:message, "def\nghi", nil) output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -94,10 +94,10 @@ def verify_parsed_events(lines:, expected_events:) verify_parsed_events( lines: [ "data:", - "" + "", ], expected_events: [ - SSE::StreamEvent.new(:message, "", nil) + SSE::StreamEvent.new(:message, "", nil), ]) end @@ -106,10 +106,10 @@ def verify_parsed_events(lines:, expected_events:) lines: [ "data:", "data: abc", - "" + "", ], expected_events: [ - SSE::StreamEvent.new(:message, "\nabc", nil) + SSE::StreamEvent.new(:message, "\nabc", nil), ]) end @@ -119,10 +119,10 @@ def verify_parsed_events(lines:, expected_events:) "data:", "data:", "data: abc", - "" + "", ], expected_events: [ - SSE::StreamEvent.new(:message, "\n\nabc", nil) + SSE::StreamEvent.new(:message, "\n\nabc", nil), ]) end @@ -132,10 +132,10 @@ def verify_parsed_events(lines:, expected_events:) "data: abc", "data:", "data: def", - "" + "", ], expected_events: [ - SSE::StreamEvent.new(:message, "abc\n\ndef", nil) + SSE::StreamEvent.new(:message, "abc\n\ndef", nil), ]) end @@ -144,10 +144,10 @@ def verify_parsed_events(lines:, expected_events:) lines: [ "data: abc", "data:", - "" + "", ], expected_events: [ - SSE::StreamEvent.new(:message, "abc\n", nil) + SSE::StreamEvent.new(:message, "abc\n", nil), ]) end @@ -156,10 +156,10 @@ def verify_parsed_events(lines:, expected_events:) ":", "data: def", ":", - "" + "", ] ep = subject.new(lines) - + expected_event = SSE::StreamEvent.new(:message, "def", nil) output = ep.items.to_a expect(output).to eq([ expected_event ]) @@ -168,7 +168,7 @@ def verify_parsed_events(lines:, expected_events:) it "parses reconnect interval" do lines = [ "retry: 2500", - "" + "", ] ep = subject.new(lines) @@ -184,10 +184,10 @@ def verify_parsed_events(lines:, expected_events:) "id: 1", "", "data: ghi", - "" + "", ] ep = subject.new(lines) - + expected_event_1 = SSE::StreamEvent.new(:abc, "def", "1", "1") expected_event_2 = SSE::StreamEvent.new(:message, "ghi", nil, "1") output = ep.items.to_a @@ -199,10 +199,10 @@ def verify_parsed_events(lines:, expected_events:) "event: nothing", "", "event: nada", - "" + "", ] ep = subject.new(lines) - + output = ep.items.to_a expect(output).to eq([]) end diff --git a/spec/http_stub.rb b/spec/http_stub.rb index 1fe8da2..3724529 100644 --- a/spec/http_stub.rb +++ b/spec/http_stub.rb @@ -62,7 +62,7 @@ def create_server(port) AccessLog: [], Logger: NullLogger.new, ProxyContentHandler: proc do |req,res| - if !@connect_status.nil? + unless @connect_status.nil? res.status = @connect_status end @request_count += 1 From ccf79af7a541c976298231b7a34c5f5bd0bd8fff Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 14 Jul 2025 13:08:02 -0400 Subject: [PATCH 17/24] fix: Explicitly mark buffer variable as unfrozen (#59) In a future version of Ruby, literal strings will be frozen by default. To avoid this causing an issue (and raising deprecation warnings now), we are explicitly marking this literal string as unfrozen. --- lib/ld-eventsource/impl/buffered_line_reader.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ld-eventsource/impl/buffered_line_reader.rb b/lib/ld-eventsource/impl/buffered_line_reader.rb index 787020c..6f6f0e8 100644 --- a/lib/ld-eventsource/impl/buffered_line_reader.rb +++ b/lib/ld-eventsource/impl/buffered_line_reader.rb @@ -1,4 +1,3 @@ - module SSE module Impl class BufferedLineReader @@ -16,7 +15,7 @@ class BufferedLineReader # @return [Enumerator] an enumerator that will yield one line at a time in UTF-8 # def self.lines_from(chunks) - buffer = "".b + buffer = +"".b position = 0 line_start = 0 last_char_was_cr = false From ffd0ffaf35e4c6a14413a8a1379d03c654e42451 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:10:43 -0400 Subject: [PATCH 18/24] chore(main): release 2.2.5 (#58) :robot: I have created a release *beep* *boop* --- ## [2.2.5](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.4...2.2.5) (2025-07-14) ### Bug Fixes * Bump minimum to ruby 3.1 ([#57](https://github.com/launchdarkly/ruby-eventsource/issues/57)) ([93a9947](https://github.com/launchdarkly/ruby-eventsource/commit/93a994783aa3aa922a213670a3c6183206d8bd8d)) * Explicitly mark buffer variable as unfrozen ([#59](https://github.com/launchdarkly/ruby-eventsource/issues/59)) ([ccf79af](https://github.com/launchdarkly/ruby-eventsource/commit/ccf79af7a541c976298231b7a34c5f5bd0bd8fff)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ lib/ld-eventsource/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3a3df6f..f90529a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.4" + ".": "2.2.5" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b905d..e3702e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.5](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.4...2.2.5) (2025-07-14) + + +### Bug Fixes + +* Bump minimum to ruby 3.1 ([#57](https://github.com/launchdarkly/ruby-eventsource/issues/57)) ([93a9947](https://github.com/launchdarkly/ruby-eventsource/commit/93a994783aa3aa922a213670a3c6183206d8bd8d)) +* Explicitly mark buffer variable as unfrozen ([#59](https://github.com/launchdarkly/ruby-eventsource/issues/59)) ([ccf79af](https://github.com/launchdarkly/ruby-eventsource/commit/ccf79af7a541c976298231b7a34c5f5bd0bd8fff)) + ## [2.2.4](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.3...2.2.4) (2025-04-18) diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 7f3c615..7ae62e5 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.4" # x-release-please-version + VERSION = "2.2.5" # x-release-please-version end From 0e2e80dc1d6515c09546ab250d5122f87b3fa013 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 15 Jul 2025 11:19:09 -0400 Subject: [PATCH 19/24] fix: Update `force_encoding` to operate on unfrozen string (#60) Similar to the work done in ccf79af, we are updating this library to handle the upcoming change where strings are frozen by default. --- lib/ld-eventsource/impl/buffered_line_reader.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ld-eventsource/impl/buffered_line_reader.rb b/lib/ld-eventsource/impl/buffered_line_reader.rb index 6f6f0e8..7f37dd9 100644 --- a/lib/ld-eventsource/impl/buffered_line_reader.rb +++ b/lib/ld-eventsource/impl/buffered_line_reader.rb @@ -22,8 +22,8 @@ def self.lines_from(chunks) Enumerator.new do |gen| chunks.each do |chunk| - chunk.force_encoding("ASCII-8BIT") - buffer << chunk + chunk = chunk.dup.force_encoding("ASCII-8BIT") + buffer += chunk loop do # Search for a line break in any part of the buffer that we haven't yet seen. From 50efb0d8d6eae1c30d5fae138dfbaa230d57d3b3 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Tue, 15 Jul 2025 11:22:09 -0400 Subject: [PATCH 20/24] fix: Add `logger` as explicit dependency (#61) Starting in 3.5.0, `logger` will no longer be a part of the default gems. v1.5 of the logger was shipped with Ruby 3.1.0, so this should maintain equivalent functionality. --- ld-eventsource.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/ld-eventsource.gemspec b/ld-eventsource.gemspec index de59b25..ed6f5c2 100644 --- a/ld-eventsource.gemspec +++ b/ld-eventsource.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 3.1' + spec.add_development_dependency 'logger', '~> 1.5' spec.add_development_dependency 'rspec', '~> 3.2' spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0' spec.add_development_dependency "rubocop", "~> 1.37" From 45e20337454643f44902fa07442931264b1813e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:23:23 -0400 Subject: [PATCH 21/24] chore(main): release 2.2.6 (#62) :robot: I have created a release *beep* *boop* --- ## [2.2.6](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.5...2.2.6) (2025-07-15) ### Bug Fixes * Add `logger` as explicit dependency ([#61](https://github.com/launchdarkly/ruby-eventsource/issues/61)) ([50efb0d](https://github.com/launchdarkly/ruby-eventsource/commit/50efb0d8d6eae1c30d5fae138dfbaa230d57d3b3)) * Update `force_encoding` to operate on unfrozen string ([#60](https://github.com/launchdarkly/ruby-eventsource/issues/60)) ([0e2e80d](https://github.com/launchdarkly/ruby-eventsource/commit/0e2e80dc1d6515c09546ab250d5122f87b3fa013)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ lib/ld-eventsource/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f90529a..dbf0c48 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.5" + ".": "2.2.6" } diff --git a/CHANGELOG.md b/CHANGELOG.md index e3702e1..941e2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.6](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.5...2.2.6) (2025-07-15) + + +### Bug Fixes + +* Add `logger` as explicit dependency ([#61](https://github.com/launchdarkly/ruby-eventsource/issues/61)) ([50efb0d](https://github.com/launchdarkly/ruby-eventsource/commit/50efb0d8d6eae1c30d5fae138dfbaa230d57d3b3)) +* Update `force_encoding` to operate on unfrozen string ([#60](https://github.com/launchdarkly/ruby-eventsource/issues/60)) ([0e2e80d](https://github.com/launchdarkly/ruby-eventsource/commit/0e2e80dc1d6515c09546ab250d5122f87b3fa013)) + ## [2.2.5](https://github.com/launchdarkly/ruby-eventsource/compare/2.2.4...2.2.5) (2025-07-14) diff --git a/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 7ae62e5..aed454a 100644 --- a/lib/ld-eventsource/version.rb +++ b/lib/ld-eventsource/version.rb @@ -1,3 +1,3 @@ module SSE - VERSION = "2.2.5" # x-release-please-version + VERSION = "2.2.6" # x-release-please-version end From e92c2f879b91be91c25f79b945f881ea4a22b04b Mon Sep 17 00:00:00 2001 From: saada Date: Thu, 14 Aug 2025 13:04:43 -0400 Subject: [PATCH 22/24] Add POST/PUT request support and optional parsing features - Add support for POST/PUT HTTP methods with JSON payload - Fix proxy authentication parameter names (proxy_username/proxy_password) - Add optional parsing mode to bypass SSE parsing for raw data streaming These changes enable more flexible usage patterns while maintaining backward compatibility with existing GET-only SSE implementations. --- lib/ld-eventsource/client.rb | 29 +++++++++++++++---- lib/ld-eventsource/impl/basic_event_parser.rb | 22 ++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 lib/ld-eventsource/impl/basic_event_parser.rb diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index c018151..aee69fe 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -1,4 +1,5 @@ require "ld-eventsource/impl/backoff" +require "ld-eventsource/impl/basic_event_parser" require "ld-eventsource/impl/buffered_line_reader" require "ld-eventsource/impl/event_parser" require "ld-eventsource/events" @@ -49,6 +50,9 @@ class Client # The default value for `reconnect_reset_interval` in {#initialize}. DEFAULT_RECONNECT_RESET_INTERVAL = 60 + # The default HTTP method for requests. + DEFAULT_HTTP_METHOD = "GET" + # # Creates a new SSE client. # @@ -84,6 +88,9 @@ class Client # @param socket_factory [#open] (nil) an optional factory object for creating sockets, # if you want to use something other than the default `TCPSocket`; it must implement # `open(uri, timeout)` to return a connected `Socket` + # @param http_method [String] (DEFAULT_HTTP_METHOD) the HTTP method to use for requests + # @param http_payload [Hash] ({}) JSON payload to send with requests (only used with POST/PUT methods) + # @param parse [Boolean] (true) whether to parse SSE events or pass through raw chunks # @yieldparam [Client] client the new client instance, before opening the connection # def initialize(uri, @@ -92,17 +99,23 @@ def initialize(uri, read_timeout: DEFAULT_READ_TIMEOUT, reconnect_time: DEFAULT_RECONNECT_TIME, reconnect_reset_interval: DEFAULT_RECONNECT_RESET_INTERVAL, + http_method: DEFAULT_HTTP_METHOD, + http_payload: {}, last_event_id: nil, proxy: nil, logger: nil, - socket_factory: nil) + socket_factory: nil, + parse: true) @uri = URI(uri) @stopped = Concurrent::AtomicBoolean.new(false) @headers = headers.clone @connect_timeout = connect_timeout @read_timeout = read_timeout + @http_method = http_method + @http_payload = http_payload @logger = logger || default_logger + @parse = parse http_client_options = {} if socket_factory http_client_options["socket_class"] = socket_factory @@ -121,6 +134,8 @@ def initialize(uri, http_client_options["proxy"] = { :proxy_address => @proxy.host, :proxy_port => @proxy.port, + :proxy_username => @proxy.user, + :proxy_password => @proxy.password, } end @@ -262,9 +277,9 @@ def connect cxn = nil begin @logger.info { "Connecting to event stream at #{@uri}" } - cxn = @http_client.request("GET", @uri, { - headers: build_headers, - }) + opts = { headers: build_headers } + opts[:json] = @http_payload unless @http_payload.empty? + cxn = @http_client.request(@http_method, @uri, opts) if cxn.status.code == 200 content_type = cxn.content_type.mime_type if content_type && content_type.start_with?("text/event-stream") @@ -316,7 +331,11 @@ def read_stream(cxn) end end end - event_parser = Impl::EventParser.new(Impl::BufferedLineReader.lines_from(chunks), @last_id) + if @parse + event_parser = Impl::EventParser.new(Impl::BufferedLineReader.lines_from(chunks), @last_id) + else + event_parser = Impl::BasicEventParser.new(chunks) + end event_parser.items.each do |item| return if @stopped.value diff --git a/lib/ld-eventsource/impl/basic_event_parser.rb b/lib/ld-eventsource/impl/basic_event_parser.rb new file mode 100644 index 0000000..3eb0533 --- /dev/null +++ b/lib/ld-eventsource/impl/basic_event_parser.rb @@ -0,0 +1,22 @@ +require "ld-eventsource/events" + +module SSE + module Impl + class BasicEventParser + + def initialize(chunks) + @chunks = chunks + end + + # Generator that parses the input iterator and returns instances of {StreamEvent} or {SetRetryInterval}. + def items + Enumerator.new do |gen| + @chunks.each do |chunk| + item = StreamEvent.new(chunk.nil? ? :final_message : :message, chunk, nil, nil) + gen.yield item + end + end + end + end + end +end \ No newline at end of file From c4c777421fd836580691f83534cf0b0b7c3d57d6 Mon Sep 17 00:00:00 2001 From: saada Date: Thu, 14 Aug 2025 13:30:27 -0400 Subject: [PATCH 23/24] Add optional SSL verification override Add verify_ssl parameter (defaults to true) to allow disabling SSL certificate verification for development, testing, and internal networks. This maintains security by default while providing flexibility when needed. --- lib/ld-eventsource/client.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index aee69fe..c4dd44a 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -7,6 +7,7 @@ require "concurrent/atomics" require "logger" +require "openssl" require "thread" require "uri" require "http" @@ -91,6 +92,7 @@ class Client # @param http_method [String] (DEFAULT_HTTP_METHOD) the HTTP method to use for requests # @param http_payload [Hash] ({}) JSON payload to send with requests (only used with POST/PUT methods) # @param parse [Boolean] (true) whether to parse SSE events or pass through raw chunks + # @param verify_ssl [Boolean] (true) whether to verify SSL certificates; set to false for development/testing # @yieldparam [Client] client the new client instance, before opening the connection # def initialize(uri, @@ -105,7 +107,8 @@ def initialize(uri, proxy: nil, logger: nil, socket_factory: nil, - parse: true) + parse: true, + verify_ssl: true) @uri = URI(uri) @stopped = Concurrent::AtomicBoolean.new(false) @@ -117,6 +120,9 @@ def initialize(uri, @logger = logger || default_logger @parse = parse http_client_options = {} + unless verify_ssl + http_client_options[:ssl] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } + end if socket_factory http_client_options["socket_class"] = socket_factory end From c0b60613ae27a13af4731e443207e80fd4908e38 Mon Sep 17 00:00:00 2001 From: saada Date: Thu, 14 Aug 2025 13:40:54 -0400 Subject: [PATCH 24/24] Fix RuboCop style violation Remove extra empty line at class body beginning in BasicEventParser. --- lib/ld-eventsource/impl/basic_event_parser.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ld-eventsource/impl/basic_event_parser.rb b/lib/ld-eventsource/impl/basic_event_parser.rb index 3eb0533..83b2e44 100644 --- a/lib/ld-eventsource/impl/basic_event_parser.rb +++ b/lib/ld-eventsource/impl/basic_event_parser.rb @@ -3,7 +3,6 @@ module SSE module Impl class BasicEventParser - def initialize(chunks) @chunks = chunks end