diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 77880fe..052d78b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.17.1" + ".": "4.18.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 5fa6994..4fd0815 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 134 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-97bb4b698571b6dbe884e93397d14aff0ec7e7279de272a15fa0defb3b2515d5.yml -openapi_spec_hash: c33bf8733151f4f5693788b6e57a8344 -config_hash: 74aad10d1512ec69541ece3ca51cf7fa +configured_endpoints: 135 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-e593915c67b3e0e375b6f9cf57d9931f86bc3ee4fd6759ba0e8098d5421fa04e.yml +openapi_spec_hash: 66cc0ce5dda5bda9f416ea3e53c3f5a0 +config_hash: 66d7703eac15d2affc326ac25b55bcd6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d25cfc..a31d79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 4.18.0 (2026-06-30) + +Full Changelog: [v4.17.1...v4.18.0](https://github.com/trycourier/courier-ruby/compare/v4.17.1...v4.18.0) + +### Features + +* **openapi:** Journeys cancel-by-token endpoint + send-node experiments (C-18986) ([12613c6](https://github.com/trycourier/courier-ruby/commit/12613c65e9b4facc19f7fa23cc92fac0b2ea8684)) + + +### Chores + +* **internal:** bound formatter parallelism to CPU count ([192d613](https://github.com/trycourier/courier-ruby/commit/192d6133c29e2ae7bfcd7c54bd1c244894f01b94)) + ## 4.17.1 (2026-06-25) Full Changelog: [v4.17.0...v4.17.1](https://github.com/trycourier/courier-ruby/compare/v4.17.0...v4.17.1) diff --git a/Gemfile.lock b/Gemfile.lock index 1908ea7..df5592f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - trycourier (4.17.1) + trycourier (4.18.0) cgi connection_pool diff --git a/Rakefile b/Rakefile index bb11fe3..2985f9d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "etc" require "pathname" require "securerandom" require "shellwords" @@ -37,7 +38,11 @@ multitask(:test) do ruby(*%w[-w -e], rb, verbose: false) { fail unless _1 } end -xargs = %w[xargs --no-run-if-empty --null --max-procs=0 --max-args=300 --] +# Cap parallelism at the CPU count. `--max-procs=0` spawns one process per +# 300-file batch with no upper bound; on large SDKs (thousands of files) that +# oversubscribes CPUs and stacks up rubocop processes, exhausting memory and +# slowing CI to the point of timing out. +xargs = %W[xargs --no-run-if-empty --null --max-procs=#{Etc.nprocessors} --max-args=300 --] ruby_opt = {"RUBYOPT" => [ENV["RUBYOPT"], "--encoding=UTF-8"].compact.join(" ")} filtered = ->(ext, dirs) do diff --git a/lib/courier.rb b/lib/courier.rb index 074e769..28e16cf 100644 --- a/lib/courier.rb +++ b/lib/courier.rb @@ -58,6 +58,7 @@ require_relative "courier/models/base_check" require_relative "courier/models/subscription_topic_new" require_relative "courier/models/elemental_channel_node" +require_relative "courier/models/cancel_journey_request" require_relative "courier/models/create_journey_request" require_relative "courier/models/journeys_invoke_request" require_relative "courier/models/journey_publish_request" @@ -131,6 +132,7 @@ require_relative "courier/models/bulk_retrieve_job_params" require_relative "courier/models/bulk_retrieve_job_response" require_relative "courier/models/bulk_run_job_params" +require_relative "courier/models/cancel_journey_response" require_relative "courier/models/channel" require_relative "courier/models/channel_classification" require_relative "courier/models/channel_metadata" @@ -171,6 +173,7 @@ require_relative "courier/models/journey_ai_node" require_relative "courier/models/journey_api_invoke_trigger_node" require_relative "courier/models/journey_archive_params" +require_relative "courier/models/journey_cancel_params" require_relative "courier/models/journey_condition_atom" require_relative "courier/models/journey_condition_group" require_relative "courier/models/journey_condition_nested_group" @@ -179,6 +182,8 @@ require_relative "courier/models/journey_delay_duration_node" require_relative "courier/models/journey_delay_until_node" require_relative "courier/models/journey_exit_node" +require_relative "courier/models/journey_experiment" +require_relative "courier/models/journey_experiment_variant" require_relative "courier/models/journey_fetch_get_delete_node" require_relative "courier/models/journey_fetch_post_put_node" require_relative "courier/models/journey_invoke_params" diff --git a/lib/courier/models.rb b/lib/courier/models.rb index c86f8ec..5c72eb9 100644 --- a/lib/courier/models.rb +++ b/lib/courier/models.rb @@ -123,6 +123,10 @@ module Courier BulkRunJobParams = Courier::Models::BulkRunJobParams + CancelJourneyRequest = Courier::Models::CancelJourneyRequest + + CancelJourneyResponse = Courier::Models::CancelJourneyResponse + Channel = Courier::Models::Channel ChannelClassification = Courier::Models::ChannelClassification @@ -207,6 +211,8 @@ module Courier JourneyArchiveParams = Courier::Models::JourneyArchiveParams + JourneyCancelParams = Courier::Models::JourneyCancelParams + # @type [Courier::Internal::Type::Converter] JourneyConditionAtom = Courier::Models::JourneyConditionAtom @@ -224,6 +230,10 @@ module Courier JourneyExitNode = Courier::Models::JourneyExitNode + JourneyExperiment = Courier::Models::JourneyExperiment + + JourneyExperimentVariant = Courier::Models::JourneyExperimentVariant + JourneyFetchGetDeleteNode = Courier::Models::JourneyFetchGetDeleteNode JourneyFetchPostPutNode = Courier::Models::JourneyFetchPostPutNode diff --git a/lib/courier/models/cancel_journey_request.rb b/lib/courier/models/cancel_journey_request.rb new file mode 100644 index 0000000..e9947b5 --- /dev/null +++ b/lib/courier/models/cancel_journey_request.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Courier + module Models + # Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of + # `cancelation_token` (cancels every run associated with the token) or `run_id` + # (cancels a single tenant-scoped run). + module CancelJourneyRequest + extend Courier::Internal::Type::Union + + variant -> { Courier::CancelJourneyRequest::ByCancelationToken } + + variant -> { Courier::CancelJourneyRequest::ByRunID } + + class ByCancelationToken < Courier::Internal::Type::BaseModel + # @!attribute cancelation_token + # + # @return [String] + required :cancelation_token, String + + # @!method initialize(cancelation_token:) + # @param cancelation_token [String] + end + + class ByRunID < Courier::Internal::Type::BaseModel + # @!attribute run_id + # + # @return [String] + required :run_id, String + + # @!method initialize(run_id:) + # @param run_id [String] + end + + # @!method self.variants + # @return [Array(Courier::Models::CancelJourneyRequest::ByCancelationToken, Courier::Models::CancelJourneyRequest::ByRunID)] + end + end +end diff --git a/lib/courier/models/cancel_journey_response.rb b/lib/courier/models/cancel_journey_response.rb new file mode 100644 index 0000000..34d4157 --- /dev/null +++ b/lib/courier/models/cancel_journey_response.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Courier + module Models + # `202 Accepted` body for `POST /journeys/cancel`, echoing the submitted + # identifier. The token branch returns `{ cancelation_token }`; the run_id branch + # returns `{ run_id, status }`. + # + # @see Courier::Resources::Journeys#cancel + module CancelJourneyResponse + extend Courier::Internal::Type::Union + + variant -> { Courier::CancelJourneyResponse::TokenBranch } + + variant -> { Courier::CancelJourneyResponse::RunIDBranch } + + class TokenBranch < Courier::Internal::Type::BaseModel + # @!attribute cancelation_token + # + # @return [String] + required :cancelation_token, String + + # @!method initialize(cancelation_token:) + # @param cancelation_token [String] + end + + class RunIDBranch < Courier::Internal::Type::BaseModel + # @!attribute run_id + # + # @return [String] + required :run_id, String + + # @!attribute status + # The run's resulting status. `CANCELED` when the run was active and we canceled + # it; `PROCESSED` or `ERROR` when the run had already finished and was left + # untouched; `CANCELED` for an already-canceled run. + # + # @return [String] + required :status, String + + # @!method initialize(run_id:, status:) + # Some parameter documentations has been truncated, see + # {Courier::Models::CancelJourneyResponse::RunIDBranch} for more details. + # + # @param run_id [String] + # + # @param status [String] The run's resulting status. `CANCELED` when the run was active and we canceled i + end + + # @!method self.variants + # @return [Array(Courier::Models::CancelJourneyResponse::TokenBranch, Courier::Models::CancelJourneyResponse::RunIDBranch)] + end + end +end diff --git a/lib/courier/models/journey_cancel_params.rb b/lib/courier/models/journey_cancel_params.rb new file mode 100644 index 0000000..5301099 --- /dev/null +++ b/lib/courier/models/journey_cancel_params.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Courier + module Models + # @see Courier::Resources::Journeys#cancel + class JourneyCancelParams < Courier::Internal::Type::BaseModel + extend Courier::Internal::Type::RequestParameters::Converter + include Courier::Internal::Type::RequestParameters + + # @!method initialize(request_options: {}) + # @param request_options [Courier::RequestOptions, Hash{Symbol=>Object}] + end + end +end diff --git a/lib/courier/models/journey_experiment.rb b/lib/courier/models/journey_experiment.rb new file mode 100644 index 0000000..543e4ab --- /dev/null +++ b/lib/courier/models/journey_experiment.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Courier + module Models + class JourneyExperiment < Courier::Internal::Type::BaseModel + # @!attribute bucketing_key + # The value used to deterministically assign a recipient to a variant. Must be + # non-empty with no leading or trailing whitespace. + # + # @return [String] + required :bucketing_key, String, api_name: :bucketingKey + + # @!attribute variants + # Between 2 and 10 weighted template variants. + # + # @return [Array] + required :variants, -> { Courier::Internal::Type::ArrayOf[Courier::JourneyExperimentVariant] } + + # @!attribute id + # Server-authoritative experiment id (prefixed `exp_`). Omit to have the server + # mint one; when supplied it must be a valid `exp_` id. + # + # @return [String, nil] + optional :id, String + + # @!attribute name + # Optional, cosmetic display name for the experiment. + # + # @return [String, nil] + optional :name, String + + # @!method initialize(bucketing_key:, variants:, id: nil, name: nil) + # Some parameter documentations has been truncated, see + # {Courier::Models::JourneyExperiment} for more details. + # + # A/B experiment config for a send node. The recipient is deterministically + # bucketed by `bucketingKey` and routed to one of the `variants` in proportion to + # its `weight`. Present on a send node INSTEAD OF `message.template`. + # + # @param bucketing_key [String] The value used to deterministically assign a recipient to a variant. Must be non + # + # @param variants [Array] Between 2 and 10 weighted template variants. + # + # @param id [String] Server-authoritative experiment id (prefixed `exp_`). Omit to have the server mi + # + # @param name [String] Optional, cosmetic display name for the experiment. + end + end +end diff --git a/lib/courier/models/journey_experiment_variant.rb b/lib/courier/models/journey_experiment_variant.rb new file mode 100644 index 0000000..bdf31df --- /dev/null +++ b/lib/courier/models/journey_experiment_variant.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Courier + module Models + class JourneyExperimentVariant < Courier::Internal::Type::BaseModel + # @!attribute id + # + # @return [String] + required :id, String + + # @!attribute template_id + # The notification template sent for this variant. + # + # @return [String] + required :template_id, String, api_name: :templateId + + # @!attribute weight + # Relative routing weight. Must be non-negative. + # + # @return [Float] + required :weight, Float + + # @!attribute name + # Optional, cosmetic display name for the variant. + # + # @return [String, nil] + optional :name, String + + # @!method initialize(id:, template_id:, weight:, name: nil) + # A single weighted arm of an experiment. Variant ids must be unique within the + # experiment and the sum of all variant weights must be greater than 0. Weights + # are relative (no sum-to-100 requirement) — routing normalizes them + # proportionally. + # + # @param id [String] + # + # @param template_id [String] The notification template sent for this variant. + # + # @param weight [Float] Relative routing weight. Must be non-negative. + # + # @param name [String] Optional, cosmetic display name for the variant. + end + end +end diff --git a/lib/courier/models/journey_node.rb b/lib/courier/models/journey_node.rb index 829f2da..e41f188 100644 --- a/lib/courier/models/journey_node.rb +++ b/lib/courier/models/journey_node.rb @@ -14,7 +14,7 @@ module JourneyNode # Trigger fired by a segment event (`identify`, `group`, or `track`). variant -> { Courier::JourneySegmentTriggerNode } - # Send a notification template to the recipient. Optionally override the recipient address, delay the send, or attach `data`. + # Send to the recipient. A send node sources its content from EXACTLY ONE of `message.template` (a single notification template) or `experiment` (an A/B split across weighted template variants) — supplying both, or neither, is rejected. Optionally override the recipient address, delay the send, or attach `data`. variant -> { Courier::JourneySendNode } # Pause the journey run for a fixed `duration`. diff --git a/lib/courier/models/journey_send_node.rb b/lib/courier/models/journey_send_node.rb index 3f67bb8..f465a2b 100644 --- a/lib/courier/models/journey_send_node.rb +++ b/lib/courier/models/journey_send_node.rb @@ -26,12 +26,23 @@ class JourneySendNode < Courier::Internal::Type::BaseModel # @return [Array, Courier::Models::JourneyConditionGroup, Courier::Models::JourneyConditionNestedGroup, nil] optional :conditions, union: -> { Courier::JourneyConditionsField } - # @!method initialize(message:, type:, id: nil, conditions: nil) + # @!attribute experiment + # A/B experiment config for a send node. The recipient is deterministically + # bucketed by `bucketingKey` and routed to one of the `variants` in proportion to + # its `weight`. Present on a send node INSTEAD OF `message.template`. + # + # @return [Courier::Models::JourneyExperiment, nil] + optional :experiment, -> { Courier::JourneyExperiment } + + # @!method initialize(message:, type:, id: nil, conditions: nil, experiment: nil) # Some parameter documentations has been truncated, see # {Courier::Models::JourneySendNode} for more details. # - # Send a notification template to the recipient. Optionally override the recipient - # address, delay the send, or attach `data`. + # Send to the recipient. A send node sources its content from EXACTLY ONE of + # `message.template` (a single notification template) or `experiment` (an A/B + # split across weighted template variants) — supplying both, or neither, is + # rejected. Optionally override the recipient address, delay the send, or attach + # `data`. # # @param message [Courier::Models::JourneySendNode::Message] # @@ -40,14 +51,11 @@ class JourneySendNode < Courier::Internal::Type::BaseModel # @param id [String] # # @param conditions [Array, Courier::Models::JourneyConditionGroup, Courier::Models::JourneyConditionNestedGroup] Condition spec for a journey node. Accepts a single condition atom, an AND/OR gr + # + # @param experiment [Courier::Models::JourneyExperiment] A/B experiment config for a send node. The recipient is deterministically bucket # @see Courier::Models::JourneySendNode#message class Message < Courier::Internal::Type::BaseModel - # @!attribute template - # - # @return [String] - required :template, String - # @!attribute data # # @return [Hash{Symbol=>Object}, nil] @@ -58,15 +66,20 @@ class Message < Courier::Internal::Type::BaseModel # @return [Courier::Models::JourneySendNode::Message::Delay, nil] optional :delay, -> { Courier::JourneySendNode::Message::Delay } + # @!attribute template + # + # @return [String, nil] + optional :template, String + # @!attribute to # # @return [Courier::Models::JourneySendNode::Message::To, nil] optional :to, -> { Courier::JourneySendNode::Message::To } - # @!method initialize(template:, data: nil, delay: nil, to: nil) - # @param template [String] + # @!method initialize(data: nil, delay: nil, template: nil, to: nil) # @param data [Hash{Symbol=>Object}] # @param delay [Courier::Models::JourneySendNode::Message::Delay] + # @param template [String] # @param to [Courier::Models::JourneySendNode::Message::To] # @see Courier::Models::JourneySendNode::Message#delay diff --git a/lib/courier/resources/journeys.rb b/lib/courier/resources/journeys.rb index 63f02de..60410a4 100644 --- a/lib/courier/resources/journeys.rb +++ b/lib/courier/resources/journeys.rb @@ -114,6 +114,37 @@ def archive(template_id, params = {}) ) end + # Some parameter documentations has been truncated, see + # {Courier::Models::JourneyCancelParams} for more details. + # + # Cancel journey runs. The request body must contain EXACTLY ONE of + # `cancelation_token` (cancels every run associated with the token) or `run_id` + # (cancels a single tenant-scoped run). Supplying both or neither is a `400`. A + # `run_id` that does not exist for the caller's tenant returns `404`. Cancelation + # is idempotent and non-clobbering: a run that has already finished + # (`PROCESSED`/`ERROR`) or was already `CANCELED` is left untouched and its + # current status is echoed back. + # + # @overload cancel(cancel_journey_request:, request_options: {}) + # + # @param cancel_journey_request [Courier::Models::CancelJourneyRequest::ByCancelationToken, Courier::Models::CancelJourneyRequest::ByRunID] Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of `cancelation_to + # + # @param request_options [Courier::RequestOptions, Hash{Symbol=>Object}, nil] + # + # @return [Courier::Models::CancelJourneyResponse::TokenBranch, Courier::Models::CancelJourneyResponse::RunIDBranch] + # + # @see Courier::Models::JourneyCancelParams + def cancel(params) + parsed, options = Courier::JourneyCancelParams.dump_request(params) + @client.request( + method: :post, + path: "journeys/cancel", + body: parsed[:cancel_journey_request], + model: Courier::CancelJourneyResponse, + options: options + ) + end + # Some parameter documentations has been truncated, see # {Courier::Models::JourneyInvokeParams} for more details. # diff --git a/lib/courier/version.rb b/lib/courier/version.rb index e781936..f792a29 100644 --- a/lib/courier/version.rb +++ b/lib/courier/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Courier - VERSION = "4.17.1" + VERSION = "4.18.0" end diff --git a/rbi/courier/models.rbi b/rbi/courier/models.rbi index f578db1..a111354 100644 --- a/rbi/courier/models.rbi +++ b/rbi/courier/models.rbi @@ -87,6 +87,10 @@ module Courier BulkRunJobParams = Courier::Models::BulkRunJobParams + CancelJourneyRequest = Courier::Models::CancelJourneyRequest + + CancelJourneyResponse = Courier::Models::CancelJourneyResponse + Channel = Courier::Models::Channel ChannelClassification = Courier::Models::ChannelClassification @@ -171,6 +175,8 @@ module Courier JourneyArchiveParams = Courier::Models::JourneyArchiveParams + JourneyCancelParams = Courier::Models::JourneyCancelParams + JourneyConditionAtom = T.let( Courier::Models::JourneyConditionAtom, @@ -191,6 +197,10 @@ module Courier JourneyExitNode = Courier::Models::JourneyExitNode + JourneyExperiment = Courier::Models::JourneyExperiment + + JourneyExperimentVariant = Courier::Models::JourneyExperimentVariant + JourneyFetchGetDeleteNode = Courier::Models::JourneyFetchGetDeleteNode JourneyFetchPostPutNode = Courier::Models::JourneyFetchPostPutNode diff --git a/rbi/courier/models/cancel_journey_request.rbi b/rbi/courier/models/cancel_journey_request.rbi new file mode 100644 index 0000000..ab5d4f9 --- /dev/null +++ b/rbi/courier/models/cancel_journey_request.rbi @@ -0,0 +1,68 @@ +# typed: strong + +module Courier + module Models + # Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of + # `cancelation_token` (cancels every run associated with the token) or `run_id` + # (cancels a single tenant-scoped run). + module CancelJourneyRequest + extend Courier::Internal::Type::Union + + Variants = + T.type_alias do + T.any( + Courier::CancelJourneyRequest::ByCancelationToken, + Courier::CancelJourneyRequest::ByRunID + ) + end + + class ByCancelationToken < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Courier::CancelJourneyRequest::ByCancelationToken, + Courier::Internal::AnyHash + ) + end + + sig { returns(String) } + attr_accessor :cancelation_token + + sig { params(cancelation_token: String).returns(T.attached_class) } + def self.new(cancelation_token:) + end + + sig { override.returns({ cancelation_token: String }) } + def to_hash + end + end + + class ByRunID < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Courier::CancelJourneyRequest::ByRunID, + Courier::Internal::AnyHash + ) + end + + sig { returns(String) } + attr_accessor :run_id + + sig { params(run_id: String).returns(T.attached_class) } + def self.new(run_id:) + end + + sig { override.returns({ run_id: String }) } + def to_hash + end + end + + sig do + override.returns(T::Array[Courier::CancelJourneyRequest::Variants]) + end + def self.variants + end + end + end +end diff --git a/rbi/courier/models/cancel_journey_response.rbi b/rbi/courier/models/cancel_journey_response.rbi new file mode 100644 index 0000000..af5acdb --- /dev/null +++ b/rbi/courier/models/cancel_journey_response.rbi @@ -0,0 +1,80 @@ +# typed: strong + +module Courier + module Models + # `202 Accepted` body for `POST /journeys/cancel`, echoing the submitted + # identifier. The token branch returns `{ cancelation_token }`; the run_id branch + # returns `{ run_id, status }`. + module CancelJourneyResponse + extend Courier::Internal::Type::Union + + Variants = + T.type_alias do + T.any( + Courier::CancelJourneyResponse::TokenBranch, + Courier::CancelJourneyResponse::RunIDBranch + ) + end + + class TokenBranch < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Courier::CancelJourneyResponse::TokenBranch, + Courier::Internal::AnyHash + ) + end + + sig { returns(String) } + attr_accessor :cancelation_token + + sig { params(cancelation_token: String).returns(T.attached_class) } + def self.new(cancelation_token:) + end + + sig { override.returns({ cancelation_token: String }) } + def to_hash + end + end + + class RunIDBranch < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Courier::CancelJourneyResponse::RunIDBranch, + Courier::Internal::AnyHash + ) + end + + sig { returns(String) } + attr_accessor :run_id + + # The run's resulting status. `CANCELED` when the run was active and we canceled + # it; `PROCESSED` or `ERROR` when the run had already finished and was left + # untouched; `CANCELED` for an already-canceled run. + sig { returns(String) } + attr_accessor :status + + sig { params(run_id: String, status: String).returns(T.attached_class) } + def self.new( + run_id:, + # The run's resulting status. `CANCELED` when the run was active and we canceled + # it; `PROCESSED` or `ERROR` when the run had already finished and was left + # untouched; `CANCELED` for an already-canceled run. + status: + ) + end + + sig { override.returns({ run_id: String, status: String }) } + def to_hash + end + end + + sig do + override.returns(T::Array[Courier::CancelJourneyResponse::Variants]) + end + def self.variants + end + end + end +end diff --git a/rbi/courier/models/journey_cancel_params.rbi b/rbi/courier/models/journey_cancel_params.rbi new file mode 100644 index 0000000..1f39bed --- /dev/null +++ b/rbi/courier/models/journey_cancel_params.rbi @@ -0,0 +1,27 @@ +# typed: strong + +module Courier + module Models + class JourneyCancelParams < Courier::Internal::Type::BaseModel + extend Courier::Internal::Type::RequestParameters::Converter + include Courier::Internal::Type::RequestParameters + + OrHash = + T.type_alias do + T.any(Courier::JourneyCancelParams, Courier::Internal::AnyHash) + end + + sig do + params(request_options: Courier::RequestOptions::OrHash).returns( + T.attached_class + ) + end + def self.new(request_options: {}) + end + + sig { override.returns({ request_options: Courier::RequestOptions }) } + def to_hash + end + end + end +end diff --git a/rbi/courier/models/journey_experiment.rbi b/rbi/courier/models/journey_experiment.rbi new file mode 100644 index 0000000..a78e75d --- /dev/null +++ b/rbi/courier/models/journey_experiment.rbi @@ -0,0 +1,74 @@ +# typed: strong + +module Courier + module Models + class JourneyExperiment < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any(Courier::JourneyExperiment, Courier::Internal::AnyHash) + end + + # The value used to deterministically assign a recipient to a variant. Must be + # non-empty with no leading or trailing whitespace. + sig { returns(String) } + attr_accessor :bucketing_key + + # Between 2 and 10 weighted template variants. + sig { returns(T::Array[Courier::JourneyExperimentVariant]) } + attr_accessor :variants + + # Server-authoritative experiment id (prefixed `exp_`). Omit to have the server + # mint one; when supplied it must be a valid `exp_` id. + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { params(id: String).void } + attr_writer :id + + # Optional, cosmetic display name for the experiment. + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + # A/B experiment config for a send node. The recipient is deterministically + # bucketed by `bucketingKey` and routed to one of the `variants` in proportion to + # its `weight`. Present on a send node INSTEAD OF `message.template`. + sig do + params( + bucketing_key: String, + variants: T::Array[Courier::JourneyExperimentVariant::OrHash], + id: String, + name: String + ).returns(T.attached_class) + end + def self.new( + # The value used to deterministically assign a recipient to a variant. Must be + # non-empty with no leading or trailing whitespace. + bucketing_key:, + # Between 2 and 10 weighted template variants. + variants:, + # Server-authoritative experiment id (prefixed `exp_`). Omit to have the server + # mint one; when supplied it must be a valid `exp_` id. + id: nil, + # Optional, cosmetic display name for the experiment. + name: nil + ) + end + + sig do + override.returns( + { + bucketing_key: String, + variants: T::Array[Courier::JourneyExperimentVariant], + id: String, + name: String + } + ) + end + def to_hash + end + end + end +end diff --git a/rbi/courier/models/journey_experiment_variant.rbi b/rbi/courier/models/journey_experiment_variant.rbi new file mode 100644 index 0000000..462c21b --- /dev/null +++ b/rbi/courier/models/journey_experiment_variant.rbi @@ -0,0 +1,61 @@ +# typed: strong + +module Courier + module Models + class JourneyExperimentVariant < Courier::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any(Courier::JourneyExperimentVariant, Courier::Internal::AnyHash) + end + + sig { returns(String) } + attr_accessor :id + + # The notification template sent for this variant. + sig { returns(String) } + attr_accessor :template_id + + # Relative routing weight. Must be non-negative. + sig { returns(Float) } + attr_accessor :weight + + # Optional, cosmetic display name for the variant. + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + # A single weighted arm of an experiment. Variant ids must be unique within the + # experiment and the sum of all variant weights must be greater than 0. Weights + # are relative (no sum-to-100 requirement) — routing normalizes them + # proportionally. + sig do + params( + id: String, + template_id: String, + weight: Float, + name: String + ).returns(T.attached_class) + end + def self.new( + id:, + # The notification template sent for this variant. + template_id:, + # Relative routing weight. Must be non-negative. + weight:, + # Optional, cosmetic display name for the variant. + name: nil + ) + end + + sig do + override.returns( + { id: String, template_id: String, weight: Float, name: String } + ) + end + def to_hash + end + end + end +end diff --git a/rbi/courier/models/journey_send_node.rbi b/rbi/courier/models/journey_send_node.rbi index 4b2a62f..e6c1d48 100644 --- a/rbi/courier/models/journey_send_node.rbi +++ b/rbi/courier/models/journey_send_node.rbi @@ -51,8 +51,20 @@ module Courier end attr_writer :conditions - # Send a notification template to the recipient. Optionally override the recipient - # address, delay the send, or attach `data`. + # A/B experiment config for a send node. The recipient is deterministically + # bucketed by `bucketingKey` and routed to one of the `variants` in proportion to + # its `weight`. Present on a send node INSTEAD OF `message.template`. + sig { returns(T.nilable(Courier::JourneyExperiment)) } + attr_reader :experiment + + sig { params(experiment: Courier::JourneyExperiment::OrHash).void } + attr_writer :experiment + + # Send to the recipient. A send node sources its content from EXACTLY ONE of + # `message.template` (a single notification template) or `experiment` (an A/B + # split across weighted template variants) — supplying both, or neither, is + # rejected. Optionally override the recipient address, delay the send, or attach + # `data`. sig do params( message: Courier::JourneySendNode::Message::OrHash, @@ -63,7 +75,8 @@ module Courier T::Array[String], Courier::JourneyConditionGroup::OrHash, Courier::JourneyConditionNestedGroup::OrHash - ) + ), + experiment: Courier::JourneyExperiment::OrHash ).returns(T.attached_class) end def self.new( @@ -73,7 +86,11 @@ module Courier # Condition spec for a journey node. Accepts a single condition atom, an AND/OR # group, or an AND/OR nested group. Omit the `conditions` property entirely to # express "no conditions". - conditions: nil + conditions: nil, + # A/B experiment config for a send node. The recipient is deterministically + # bucketed by `bucketingKey` and routed to one of the `variants` in proportion to + # its `weight`. Present on a send node INSTEAD OF `message.template`. + experiment: nil ) end @@ -88,7 +105,8 @@ module Courier T::Array[String], Courier::JourneyConditionGroup, Courier::JourneyConditionNestedGroup - ) + ), + experiment: Courier::JourneyExperiment } ) end @@ -101,9 +119,6 @@ module Courier T.any(Courier::JourneySendNode::Message, Courier::Internal::AnyHash) end - sig { returns(String) } - attr_accessor :template - sig { returns(T.nilable(T::Hash[Symbol, T.anything])) } attr_reader :data @@ -118,6 +133,12 @@ module Courier end attr_writer :delay + sig { returns(T.nilable(String)) } + attr_reader :template + + sig { params(template: String).void } + attr_writer :template + sig { returns(T.nilable(Courier::JourneySendNode::Message::To)) } attr_reader :to @@ -126,21 +147,21 @@ module Courier sig do params( - template: String, data: T::Hash[Symbol, T.anything], delay: Courier::JourneySendNode::Message::Delay::OrHash, + template: String, to: Courier::JourneySendNode::Message::To::OrHash ).returns(T.attached_class) end - def self.new(template:, data: nil, delay: nil, to: nil) + def self.new(data: nil, delay: nil, template: nil, to: nil) end sig do override.returns( { - template: String, data: T::Hash[Symbol, T.anything], delay: Courier::JourneySendNode::Message::Delay, + template: String, to: Courier::JourneySendNode::Message::To } ) diff --git a/rbi/courier/resources/journeys.rbi b/rbi/courier/resources/journeys.rbi index d2e335d..3377ea0 100644 --- a/rbi/courier/resources/journeys.rbi +++ b/rbi/courier/resources/journeys.rbi @@ -101,6 +101,32 @@ module Courier ) end + # Cancel journey runs. The request body must contain EXACTLY ONE of + # `cancelation_token` (cancels every run associated with the token) or `run_id` + # (cancels a single tenant-scoped run). Supplying both or neither is a `400`. A + # `run_id` that does not exist for the caller's tenant returns `404`. Cancelation + # is idempotent and non-clobbering: a run that has already finished + # (`PROCESSED`/`ERROR`) or was already `CANCELED` is left untouched and its + # current status is echoed back. + sig do + params( + cancel_journey_request: + T.any( + Courier::CancelJourneyRequest::ByCancelationToken::OrHash, + Courier::CancelJourneyRequest::ByRunID::OrHash + ), + request_options: Courier::RequestOptions::OrHash + ).returns(Courier::CancelJourneyResponse::Variants) + end + def cancel( + # Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of + # `cancelation_token` (cancels every run associated with the token) or `run_id` + # (cancels a single tenant-scoped run). + cancel_journey_request:, + request_options: {} + ) + end + # Invoke a journey by id or alias to start a new run. The response includes a # `runId` identifying the run. sig do diff --git a/sig/courier/models.rbs b/sig/courier/models.rbs index 3f43fd9..20bfe1e 100644 --- a/sig/courier/models.rbs +++ b/sig/courier/models.rbs @@ -83,6 +83,10 @@ module Courier class BulkRunJobParams = Courier::Models::BulkRunJobParams + module CancelJourneyRequest = Courier::Models::CancelJourneyRequest + + module CancelJourneyResponse = Courier::Models::CancelJourneyResponse + class Channel = Courier::Models::Channel module ChannelClassification = Courier::Models::ChannelClassification @@ -167,6 +171,8 @@ module Courier class JourneyArchiveParams = Courier::Models::JourneyArchiveParams + class JourneyCancelParams = Courier::Models::JourneyCancelParams + JourneyConditionAtom: Courier::Internal::Type::Converter class JourneyConditionGroup = Courier::Models::JourneyConditionGroup @@ -183,6 +189,10 @@ module Courier class JourneyExitNode = Courier::Models::JourneyExitNode + class JourneyExperiment = Courier::Models::JourneyExperiment + + class JourneyExperimentVariant = Courier::Models::JourneyExperimentVariant + class JourneyFetchGetDeleteNode = Courier::Models::JourneyFetchGetDeleteNode class JourneyFetchPostPutNode = Courier::Models::JourneyFetchPostPutNode diff --git a/sig/courier/models/cancel_journey_request.rbs b/sig/courier/models/cancel_journey_request.rbs new file mode 100644 index 0000000..d501e1e --- /dev/null +++ b/sig/courier/models/cancel_journey_request.rbs @@ -0,0 +1,33 @@ +module Courier + module Models + type cancel_journey_request = + Courier::CancelJourneyRequest::ByCancelationToken + | Courier::CancelJourneyRequest::ByRunID + + module CancelJourneyRequest + extend Courier::Internal::Type::Union + + type by_cancelation_token = { cancelation_token: String } + + class ByCancelationToken < Courier::Internal::Type::BaseModel + attr_accessor cancelation_token: String + + def initialize: (cancelation_token: String) -> void + + def to_hash: -> { cancelation_token: String } + end + + type by_run_id = { run_id: String } + + class ByRunID < Courier::Internal::Type::BaseModel + attr_accessor run_id: String + + def initialize: (run_id: String) -> void + + def to_hash: -> { run_id: String } + end + + def self?.variants: -> ::Array[Courier::Models::cancel_journey_request] + end + end +end diff --git a/sig/courier/models/cancel_journey_response.rbs b/sig/courier/models/cancel_journey_response.rbs new file mode 100644 index 0000000..04debee --- /dev/null +++ b/sig/courier/models/cancel_journey_response.rbs @@ -0,0 +1,35 @@ +module Courier + module Models + type cancel_journey_response = + Courier::CancelJourneyResponse::TokenBranch + | Courier::CancelJourneyResponse::RunIDBranch + + module CancelJourneyResponse + extend Courier::Internal::Type::Union + + type token_branch = { cancelation_token: String } + + class TokenBranch < Courier::Internal::Type::BaseModel + attr_accessor cancelation_token: String + + def initialize: (cancelation_token: String) -> void + + def to_hash: -> { cancelation_token: String } + end + + type run_id_branch = { run_id: String, status: String } + + class RunIDBranch < Courier::Internal::Type::BaseModel + attr_accessor run_id: String + + attr_accessor status: String + + def initialize: (run_id: String, status: String) -> void + + def to_hash: -> { run_id: String, status: String } + end + + def self?.variants: -> ::Array[Courier::Models::cancel_journey_response] + end + end +end diff --git a/sig/courier/models/journey_cancel_params.rbs b/sig/courier/models/journey_cancel_params.rbs new file mode 100644 index 0000000..0151625 --- /dev/null +++ b/sig/courier/models/journey_cancel_params.rbs @@ -0,0 +1,15 @@ +module Courier + module Models + type journey_cancel_params = + { } & Courier::Internal::Type::request_parameters + + class JourneyCancelParams < Courier::Internal::Type::BaseModel + extend Courier::Internal::Type::RequestParameters::Converter + include Courier::Internal::Type::RequestParameters + + def initialize: (?request_options: Courier::request_opts) -> void + + def to_hash: -> { request_options: Courier::RequestOptions } + end + end +end diff --git a/sig/courier/models/journey_experiment.rbs b/sig/courier/models/journey_experiment.rbs new file mode 100644 index 0000000..ea4614b --- /dev/null +++ b/sig/courier/models/journey_experiment.rbs @@ -0,0 +1,39 @@ +module Courier + module Models + type journey_experiment = + { + bucketing_key: String, + variants: ::Array[Courier::JourneyExperimentVariant], + id: String, + name: String + } + + class JourneyExperiment < Courier::Internal::Type::BaseModel + attr_accessor bucketing_key: String + + attr_accessor variants: ::Array[Courier::JourneyExperimentVariant] + + attr_reader id: String? + + def id=: (String) -> String + + attr_reader name: String? + + def name=: (String) -> String + + def initialize: ( + bucketing_key: String, + variants: ::Array[Courier::JourneyExperimentVariant], + ?id: String, + ?name: String + ) -> void + + def to_hash: -> { + bucketing_key: String, + variants: ::Array[Courier::JourneyExperimentVariant], + id: String, + name: String + } + end + end +end diff --git a/sig/courier/models/journey_experiment_variant.rbs b/sig/courier/models/journey_experiment_variant.rbs new file mode 100644 index 0000000..48e0221 --- /dev/null +++ b/sig/courier/models/journey_experiment_variant.rbs @@ -0,0 +1,32 @@ +module Courier + module Models + type journey_experiment_variant = + { id: String, template_id: String, weight: Float, name: String } + + class JourneyExperimentVariant < Courier::Internal::Type::BaseModel + attr_accessor id: String + + attr_accessor template_id: String + + attr_accessor weight: Float + + attr_reader name: String? + + def name=: (String) -> String + + def initialize: ( + id: String, + template_id: String, + weight: Float, + ?name: String + ) -> void + + def to_hash: -> { + id: String, + template_id: String, + weight: Float, + name: String + } + end + end +end diff --git a/sig/courier/models/journey_send_node.rbs b/sig/courier/models/journey_send_node.rbs index de57583..1f7fa16 100644 --- a/sig/courier/models/journey_send_node.rbs +++ b/sig/courier/models/journey_send_node.rbs @@ -5,7 +5,8 @@ module Courier message: Courier::JourneySendNode::Message, type: Courier::Models::JourneySendNode::type_, id: String, - conditions: Courier::Models::journey_conditions_field + conditions: Courier::Models::journey_conditions_field, + experiment: Courier::JourneyExperiment } class JourneySendNode < Courier::Internal::Type::BaseModel @@ -23,31 +24,37 @@ module Courier Courier::Models::journey_conditions_field ) -> Courier::Models::journey_conditions_field + attr_reader experiment: Courier::JourneyExperiment? + + def experiment=: ( + Courier::JourneyExperiment + ) -> Courier::JourneyExperiment + def initialize: ( message: Courier::JourneySendNode::Message, type: Courier::Models::JourneySendNode::type_, ?id: String, - ?conditions: Courier::Models::journey_conditions_field + ?conditions: Courier::Models::journey_conditions_field, + ?experiment: Courier::JourneyExperiment ) -> void def to_hash: -> { message: Courier::JourneySendNode::Message, type: Courier::Models::JourneySendNode::type_, id: String, - conditions: Courier::Models::journey_conditions_field + conditions: Courier::Models::journey_conditions_field, + experiment: Courier::JourneyExperiment } type message = { - template: String, data: ::Hash[Symbol, top], delay: Courier::JourneySendNode::Message::Delay, + template: String, to: Courier::JourneySendNode::Message::To } class Message < Courier::Internal::Type::BaseModel - attr_accessor template: String - attr_reader data: ::Hash[Symbol, top]? def data=: (::Hash[Symbol, top]) -> ::Hash[Symbol, top] @@ -58,6 +65,10 @@ module Courier Courier::JourneySendNode::Message::Delay ) -> Courier::JourneySendNode::Message::Delay + attr_reader template: String? + + def template=: (String) -> String + attr_reader to: Courier::JourneySendNode::Message::To? def to=: ( @@ -65,16 +76,16 @@ module Courier ) -> Courier::JourneySendNode::Message::To def initialize: ( - template: String, ?data: ::Hash[Symbol, top], ?delay: Courier::JourneySendNode::Message::Delay, + ?template: String, ?to: Courier::JourneySendNode::Message::To ) -> void def to_hash: -> { - template: String, data: ::Hash[Symbol, top], delay: Courier::JourneySendNode::Message::Delay, + template: String, to: Courier::JourneySendNode::Message::To } diff --git a/sig/courier/resources/journeys.rbs b/sig/courier/resources/journeys.rbs index 74a19e6..51aa439 100644 --- a/sig/courier/resources/journeys.rbs +++ b/sig/courier/resources/journeys.rbs @@ -28,6 +28,11 @@ module Courier ?request_options: Courier::request_opts ) -> nil + def cancel: ( + cancel_journey_request: Courier::Models::cancel_journey_request, + ?request_options: Courier::request_opts + ) -> Courier::Models::cancel_journey_response + def invoke: ( String template_id, ?data: ::Hash[Symbol, top], diff --git a/test/courier/resources/journeys_test.rb b/test/courier/resources/journeys_test.rb index 589545b..46189e4 100644 --- a/test/courier/resources/journeys_test.rb +++ b/test/courier/resources/journeys_test.rb @@ -84,6 +84,23 @@ def test_archive end end + def test_cancel_required_params + skip("Mock server tests are disabled") + + response = @courier.journeys.cancel(cancel_journey_request: {cancelation_token: "order-1234"}) + + assert_pattern do + response => Courier::CancelJourneyResponse + end + + assert_pattern do + case response + in Courier::CancelJourneyResponse::TokenBranch + in Courier::CancelJourneyResponse::RunIDBranch + end + end + end + def test_invoke skip("Mock server tests are disabled")