diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 54a42de..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,67 +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: 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 - -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..94ab372 --- /dev/null +++ b/.github/actions/ci/action.yml @@ -0,0 +1,53 @@ +name: CI Workflow +description: "Shared CI workflow." +inputs: + ruby-version: + 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: + using: composite + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby-version }} + + - name: Install dependencies + shell: bash + run: bundle 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_TESTS='y'" >> $GITHUB_ENV + + - name: Run tests + shell: bash + 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: Start contract test service + if: ${{ !startsWith(inputs.ruby-version, 'jruby') }} + shell: bash + 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/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..80bf00a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +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: + fail-fast: false + matrix: + ruby-version: + - "3.2" + - "3.3" + - "3.4" + - jruby-9.4 + - jruby-10.0 + + 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 }} + token: ${{ secrets.GITHUB_TOKEN }} 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..9adcc89 --- /dev/null +++ b/.github/workflows/manual-publish.yml @@ -0,0 +1,39 @@ +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.2.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: "3.2" + token: ${{ secrets.GITHUB_TOKEN }} + + - id: publish + name: Publish Package + 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 new file mode 100644 index 0000000..8aec81b --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,42 @@ +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: googleapis/release-please-action@v4 + id: release + + - uses: actions/checkout@v4 + 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.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 == 'true' }} + with: + ruby-version: "3.2" + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: ./.github/actions/publish + if: ${{ steps.release.outputs.releases_created == 'true' }} + with: + dry_run: false + env: + GEM_HOST_API_KEY: ${{ env.GEM_HOST_API_KEY }} diff --git a/.gitignore b/.gitignore index 33d6301..0b6da8f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ mkmf.log rspec Gemfile.lock .ruby-version -.idea \ No newline at end of file diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml deleted file mode 100644 index ab2ac1f..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.5-buster - template: - name: ruby diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..dbf0c48 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2.2.6" +} 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/CHANGELOG.md b/CHANGELOG.md index 1fbb7f2..941e2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ 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) + + +### 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) + + +### 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) + + +### 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)!) + ## [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/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..71f97d0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Repository Maintainers +* @launchdarkly/team-sdk-ruby diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..27c23d0 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log + +build-contract-tests: + @cd contract-tests && bundle install + +start-contract-test-service: + @cd contract-tests && bundle 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/README.md b/README.md index 544331f..f360d15 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. @@ -10,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 new file mode 100644 index 0000000..7e4c662 --- /dev/null +++ b/contract-tests/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem 'ld-eventsource', path: '..' + +gem 'json' +gem 'puma', '~> 6.6' +gem 'rackup', '~> 2.2' +gem 'sinatra', '>= 4.1' 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..0a1d303 --- /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 |client| + entity = StreamEntity.new(client, 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..078f95a --- /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 diff --git a/ld-eventsource.gemspec b/ld-eventsource.gemspec index e9dceaa..ed6f5c2 100644 --- a/ld-eventsource.gemspec +++ b/ld-eventsource.gemspec @@ -1,29 +1,32 @@ # 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/**/*") + Dir.glob("README.md") + Dir.glob("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 '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" + 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 e7ae50d..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" @@ -50,6 +51,7 @@ 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" # @@ -87,8 +89,12 @@ 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 + # @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, headers: {}, connect_timeout: DEFAULT_CONNECT_TIMEOUT, @@ -101,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) @@ -112,15 +119,14 @@ def initialize(uri, @http_payload = http_payload @logger = logger || default_logger @parse = parse - http_client_options = { - ssl: { - verify_mode: OpenSSL::SSL::VERIFY_NONE # Ignore SSL verification - } - } + 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 - + if proxy @proxy = proxy else @@ -142,7 +148,7 @@ def initialize(uri, @http_client = HTTP::Client.new(http_client_options) .timeout({ read: read_timeout, - connect: connect_timeout + connect: connect_timeout, }) @cxn = nil @lock = Mutex.new @@ -156,9 +162,7 @@ def initialize(uri, yield self if block_given? - Thread.new do - run_stream - end + Thread.new { run_stream }.name = 'LD/SSEClient' end # @@ -217,15 +221,15 @@ def closed? end 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 @@ -233,12 +237,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 @@ -248,7 +252,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. @@ -273,24 +277,24 @@ 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 begin @logger.info { "Connecting to event stream at #{@uri}" } opts = { headers: build_headers } - opts[:json] = @http_payload unless @http_payload == {} + opts[:json] = @http_payload unless @http_payload.empty? cxn = @http_client.request(@http_method, @uri, opts) 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 @@ -324,7 +328,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 @@ -353,7 +357,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) @@ -363,7 +367,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}" } @@ -374,7 +378,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/basic_event_parser.rb b/lib/ld-eventsource/impl/basic_event_parser.rb index 2b78f5a..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 @@ -19,4 +18,4 @@ def items end end end -end +end \ No newline at end of file diff --git a/lib/ld-eventsource/impl/buffered_line_reader.rb b/lib/ld-eventsource/impl/buffered_line_reader.rb index 787020c..7f37dd9 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,15 +15,15 @@ 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 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. 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/lib/ld-eventsource/version.rb b/lib/ld-eventsource/version.rb index 806179d..aed454a 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.6" # x-release-please-version end diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..1bf4557 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,12 @@ +{ + "packages": { + ".": { + "release-type": "ruby", + "bump-minor-pre-major": true, + "versioning": "default", + "include-component-in-tag": false, + "include-v-in-tag": false, + "extra-files": ["lib/ld-eventsource/version.rb"] + } + } +} 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 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