Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

* **Writing to a duplicated options object no longer changes the original.** `client.globals.dup` shared its internal storage with the client, so writes to the copy silently reconfigured it.
* **Faraday clients no longer touch HTTPI's global logging configuration.** Creating a client stamped `HTTPI.log` and `HTTPI.logger` regardless of the chosen transport. With `transport: :faraday` HTTPI is not involved, so its process-global logging state is now left alone. Clients on the default HTTPI transport still mirror their `log`/`logger` setup to HTTPI, once at client initialization.

### Added

* **Add `future: true` global option as Savon 3.0 preview channel** ([discussion #1060](https://github.com/savonrb/savon/discussions/1060)). One opt-in flag that enables the next major version's defaults today. Every option you set explicitly still keeps winning over the future defaults. This grows with 2.x minor releases and changes will be listed here under a new "3.0 preview" section (see below). Release updates are posted in the discussion. With the flag on, the client logs one info-level line at initialization stating this contract. A `log_level` of `:warn` or higher silences it. The flag can only be set when creating a client.

### Changed

* **Minimum Nori version is now `~> 2.9`** (was `~> 2.7`). Needed for the `standards` and `serializable` parsing profiles that `future: true` enables. Independent of the flag, the Nori 2.8-2.9 series brings one default-on bugfix callers benefit from automatically: whitespace-only CDATA content is preserved as text instead of being dropped.

### 3.0 preview

New in the `future: true` preview with this release:

* **Nori `standards: true`** - spec-correct response parsing. Empty tags parse to `""` instead of `nil` (with or without attributes, `xsi:nil="true"` still wins), whitespace-only text under `xml:space="preserve"` is kept, and no types are guessed without a schema: `advanced_typecasting` defaults to off and bare `type=`/`nil=` attributes are ordinary attributes. An explicit `empty_tag_value` or `advanced_typecasting` option keeps winning.
* **Nori `serializable: true`** - plain, directly-serializable response data with no custom value classes. A tag with text and attributes parses to `{"#text" => ..., "@attr" => ...}` instead of a `Nori::StringWithAttributes`, so attributes survive `to_json`.
* **`transport: :faraday`** - the Faraday transport (introduced in 2.17.0) becomes the default under the flag. Requires the `faraday` gem. HTTPI-specific globals such as `proxy`, the timeout and `ssl_*` families, and HTTP auth are rejected at initialization with a per-option migration hint.
* **Frozen client options** - `client.globals` is frozen once the client is created. Setting a global afterwards raises a `FrozenError` explaining the contract. Mutating options after creation skipped initialization-time validation and WSDL setup and was never thread-safe. Savon 3 makes every client immutable. With `Savon::Model`, enabling the future flag in its `client(...)` configuration changes the model to record options, later `global` calls included, and create the client once on first use. Configuring the model after first use raises.

## [2.17.4] - 2026-07-03

**Restore WS-Addressing headers and fix `:wsse_signature` resolution**
Expand Down
6 changes: 6 additions & 0 deletions lib/savon/block_interface.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# frozen_string_literal: true

module Savon
# Evaluates an options block against an {Options} target.
#
# A block expecting an argument receives the target directly. A block
# without arguments is instance-evaluated, so bare setter calls reach the
# target through {#method_missing} and unknown methods fall back to the
# scope the block was defined in.
class BlockInterface
def initialize(target)
@target = target
Expand Down
17 changes: 16 additions & 1 deletion lib/savon/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "savon/transport/httpi"
require "savon/transport/faraday"
require "savon/options"
require "savon/future"
require "savon/block_interface"
require "wasabi"

Expand All @@ -20,13 +21,16 @@ def initialize(globals = {}, &block)
end

set_globals(globals, block)
@globals.validate_transport!
@globals.validate!
mirror_logging_to_httpi
Future.announce(@globals[:logger]) if @globals[:future]

unless wsdl_or_endpoint_and_namespace_specified?
raise_initialization_error!
end

build_wsdl_document
@globals.finalize!
end

attr_reader :globals, :wsdl
Expand Down Expand Up @@ -83,6 +87,17 @@ def set_globals(globals, block)
@globals = globals
end

# Mirrors the client's logging setup to HTTPI's process-global state,
# which HTTPI reads while executing requests. Runs once, after all
# options are assigned. Faraday clients leave HTTPI alone. Goes away in
# Savon 3 together with the HTTPI transport.
def mirror_logging_to_httpi
return if @globals[:transport] == :faraday

HTTPI.log = @globals[:log]
HTTPI.logger = @globals[:logger]
end

def build_wsdl_document
@wsdl = Wasabi::Document.new

Expand Down
49 changes: 49 additions & 0 deletions lib/savon/future.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Savon
# The preview channel for the next major version's defaults.
#
# Enabled by the +future: true+ global option, read by
# {Savon::GlobalOptions#future}. Everything the preview changes arrives
# as a default. An option you set explicitly wins over a previewed
# default the same way it wins over a built-in one. There is no finer
# control than that. Previewed behavior changes cannot be switched off
# individually. The preview grows with 2.x minor releases, and each
# release lists its additions under the "3.0 preview" section of the
# changelog. Version 3.0 makes the previewed behavior the default and
# the flag a no-op.
#
# The preview covers option defaults ({GLOBAL_DEFAULTS} and
# {NORI_PROFILES}) as well as behavior changes such as {Savon::Client}
# freezing its options once the client is created.
module Future
# Global options the flag overlays between the built-in defaults and
# the caller's explicit options.
GLOBAL_DEFAULTS = {
transport: :faraday
}.freeze

# Nori profiles Savon enables for response parsing under the flag.
# {Savon::Response} passes them when building its Nori instance.
NORI_PROFILES = {
standards: true,
serializable: true
}.freeze

# Where the preview channel is announced and release updates are posted.
DISCUSSION_URL = "https://github.com/savonrb/savon/discussions/1060"

# Logs the preview contract once at client initialization.
#
# The line is info-level. A +log_level+ of +:warn+ or higher, or a
# custom logger, silences it.
#
# @param logger [Logger] the logger of the client being initialized
# @return [void]
def self.announce(logger)
logger.info "Savon future: true is on. This client previews the Savon 3.0 defaults. " \
"The preview grows with 2.x minor releases. See the '3.0 preview' " \
"changelog sections and #{DISCUSSION_URL}"
end
end
end
23 changes: 21 additions & 2 deletions lib/savon/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,33 @@ def operation_method_name(operation)
# Class methods.
def class_operation_module
@class_operation_module ||= Module.new do
# Configures and returns the model's Savon::Client. The first call
# with options is the configuration, later options are ignored.
# With +future: true+ in the configuration, creation is deferred
# until first use, so options recorded by +global+ become part of
# one client. The configuring call then returns nil.
def client(globals = {})
@client ||= Savon::Client.new(globals)
if globals.any?
@client_globals ||= globals.dup
return if @client_globals[:future]
end

@client ||= Savon::Client.new(@client_globals || {})
rescue InitializationError
raise_initialization_error!
end

# Sets a single global option. With +future: true+ this records
# into the configuration of the not-yet-created client. Without
# the flag it mutates the live client, as it always has.
def global(option, *value)
client.globals[option] = value
if @client.nil? && @client_globals && @client_globals[:future]
# Constructor setters take one argument, so single values are
# unwrapped the way []= would flatten them.
@client_globals[option] = value.size == 1 ? value.first : value
else
client.globals[option] = value
end
end

def raise_initialization_error!
Expand Down
Loading