From bda7816b9d134cbf77734e6d445cb1711aa92ab0 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Fri, 23 Oct 2026 22:49:32 -0700 Subject: [PATCH] Replace multi_json with the json standard library multi_json picks between JSON backends at runtime. That mattered when the json gem was slow, but stdlib json is now as fast as or faster than oj on Docker API payloads, and calling it directly skips multi_json's per-call adapter dispatch. - JSON.parse / JSON.generate replace MultiJson.load / MultiJson.dump - JSON::ParserError replaces MultiJson::ParseError in Util.parse_json - symbolize_names: replaces multi_json's symbolize_keys: option - Drop the multi_json runtime dependency Signed-off-by: Tim Smith --- docker-api.gemspec | 1 - lib/docker.rb | 4 ++-- lib/docker/container.rb | 10 +++++----- lib/docker/exec.rb | 4 ++-- lib/docker/image.rb | 2 +- lib/docker/network.rb | 6 +++--- lib/docker/util.rb | 10 +++++----- lib/docker/volume.rb | 2 +- spec/docker/container_spec.rb | 2 +- spec/docker/event_spec.rb | 2 +- spec/docker/util_spec.rb | 6 +++--- spec/docker_spec.rb | 2 +- 12 files changed, 25 insertions(+), 26 deletions(-) diff --git a/docker-api.gemspec b/docker-api.gemspec index 86694c7b..f6f9971b 100644 --- a/docker-api.gemspec +++ b/docker-api.gemspec @@ -13,7 +13,6 @@ Gem::Specification.new do |gem| gem.name = 'docker-api' gem.version = Docker::VERSION gem.add_dependency 'excon', '>= 0.64.0' - gem.add_dependency 'multi_json' # base64 stopped being a default gem in Ruby 3.4, so `require 'base64'` in # lib/docker.rb raises LoadError under Bundler unless it is declared. gem.add_dependency 'base64' diff --git a/lib/docker.rb b/lib/docker.rb index 973e0d5e..fb5bfb4e 100644 --- a/lib/docker.rb +++ b/lib/docker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'cgi' -require 'multi_json' +require 'json' require 'excon' require 'tempfile' require 'base64' @@ -133,7 +133,7 @@ def rootless?(connection = self.connection) # Login to the Docker registry. def authenticate!(options = {}, connection = self.connection) - creds = MultiJson.dump(options) + creds = JSON.generate(options) connection.post('/auth', {}, body: creds) @creds = creds true diff --git a/lib/docker/container.rb b/lib/docker/container.rb index 3126d966..ffb1ca34 100644 --- a/lib/docker/container.rb +++ b/lib/docker/container.rb @@ -150,7 +150,7 @@ def commit(options = {}) # Based on the link, the config passed as run, needs to be passed as the # body of the post so capture it, remove from the options, and pass it via # the post body - config = MultiJson.dump(options.delete('run')) + config = JSON.generate(options.delete('run')) hash = Docker::Util.parse_json( connection.post('/commit', options, body: config) ) @@ -200,7 +200,7 @@ def rename(new_name) end def update(opts) - connection.post(path_for(:update), {}, body: MultiJson.dump(opts)) + connection.post(path_for(:update), {}, body: JSON.generate(opts)) end def streaming_logs(opts = {}, &block) @@ -214,7 +214,7 @@ def streaming_logs(opts = {}, &block) end def start!(opts = {}) - connection.post(path_for(:start), {}, body: MultiJson.dump(opts)) + connection.post(path_for(:start), {}, body: JSON.generate(opts)) self end @@ -240,7 +240,7 @@ def kill!(opts = {}) timeout = opts.delete('timeout') query = {} request_options = { - :body => MultiJson.dump(opts) + :body => JSON.generate(opts) } if timeout query['t'] = timeout @@ -346,7 +346,7 @@ def store_file(path, file_content) def self.create(opts = {}, conn = Docker.connection) query = opts.select { |key| CREATE_QUERY_KEYS.include?(key.to_s) } clean_opts = opts.reject { |key| CREATE_QUERY_KEYS.include?(key.to_s) } - resp = conn.post('/containers/create', query, :body => MultiJson.dump(clean_opts)) + resp = conn.post('/containers/create', query, :body => JSON.generate(clean_opts)) hash = Docker::Util.parse_json(resp) || {} new(conn, hash) end diff --git a/lib/docker/exec.rb b/lib/docker/exec.rb index 00394bc9..c71c8b8c 100644 --- a/lib/docker/exec.rb +++ b/lib/docker/exec.rb @@ -29,7 +29,7 @@ def self.create(options = {}, conn = Docker.connection) end resp = conn.post("/containers/#{container}/exec", {}, - body: MultiJson.dump(options)) + body: JSON.generate(options)) hash = Docker::Util.parse_json(resp) || {} new(conn, hash) end @@ -60,7 +60,7 @@ def start!(options = {}, &block) read_timeout = options[:wait] # Create API Request Body - body = MultiJson.dump( + body = JSON.generate( 'Tty' => tty, 'Detach' => detached ) diff --git a/lib/docker/image.rb b/lib/docker/image.rb index 87ac0bee..8acadbc2 100644 --- a/lib/docker/image.rb +++ b/lib/docker/image.rb @@ -119,7 +119,7 @@ class << self # Create a new Image. def create(opts = {}, creds = nil, conn = Docker.connection, &block) - credentials = creds.nil? ? Docker.creds : MultiJson.dump(creds) + credentials = creds.nil? ? Docker.creds : JSON.generate(creds) headers = credentials && Docker::Util.build_auth_header(credentials) || {} body = +'' conn.post( diff --git a/lib/docker/network.rb b/lib/docker/network.rb index b9acec86..401b15c8 100644 --- a/lib/docker/network.rb +++ b/lib/docker/network.rb @@ -5,7 +5,7 @@ class Docker::Network include Docker::Base def connect(container, opts = {}, body_opts = {}) - body = MultiJson.dump({ container: container }.merge(body_opts)) + body = JSON.generate({ container: container }.merge(body_opts)) Docker::Util.parse_json( connection.post(path_for('connect'), opts, body: body) ) @@ -13,7 +13,7 @@ def connect(container, opts = {}, body_opts = {}) end def disconnect(container, opts = {}) - body = MultiJson.dump(container: container) + body = JSON.generate(container: container) Docker::Util.parse_json( connection.post(path_for('disconnect'), opts, body: body) ) @@ -43,7 +43,7 @@ def reload class << self def create(name, opts = {}, conn = Docker.connection) - default_opts = MultiJson.dump({ + default_opts = JSON.generate({ 'Name' => name, 'CheckDuplicate' => true }.merge(opts)) diff --git a/lib/docker/util.rb b/lib/docker/util.rb index c8549232..4d595294 100644 --- a/lib/docker/util.rb +++ b/lib/docker/util.rb @@ -106,8 +106,8 @@ def close_write(socket) end def parse_json(body) - MultiJson.load(body) unless body.nil? || body.empty? || (body == 'null') - rescue MultiJson::ParseError => ex + JSON.parse(body) unless body.nil? || body.empty? || (body == 'null') + rescue JSON::ParserError => ex raise UnexpectedResponseError, ex.message end @@ -261,7 +261,7 @@ def filesystem_permissions(path) end def build_auth_header(credentials) - credentials = MultiJson.dump(credentials) if credentials.is_a?(Hash) + credentials = JSON.generate(credentials) if credentials.is_a?(Hash) encoded_creds = Base64.urlsafe_encode64(credentials) { 'X-Registry-Auth' => encoded_creds @@ -270,10 +270,10 @@ def build_auth_header(credentials) def build_config_header(credentials) if credentials.is_a?(String) - credentials = MultiJson.load(credentials, symbolize_keys: true) + credentials = JSON.parse(credentials, symbolize_names: true) end - header = MultiJson.dump( + header = JSON.generate( credentials[:serveraddress].to_s => { 'username' => credentials[:username].to_s, 'password' => credentials[:password].to_s, diff --git a/lib/docker/volume.rb b/lib/docker/volume.rb index 686f57c8..5e8089e8 100644 --- a/lib/docker/volume.rb +++ b/lib/docker/volume.rb @@ -33,7 +33,7 @@ def all(opts = {}, conn = Docker.connection) # creates a volume with an arbitrary name def create(name, opts = {}, conn = Docker.connection) opts['Name'] = name - resp = conn.post('/volumes/create', {}, body: MultiJson.dump(opts)) + resp = conn.post('/volumes/create', {}, body: JSON.generate(opts)) hash = Docker::Util.parse_json(resp) || {} new(conn, hash) end diff --git a/spec/docker/container_spec.rb b/spec/docker/container_spec.rb index d032b46c..6e5bf172 100644 --- a/spec/docker/container_spec.rb +++ b/spec/docker/container_spec.rb @@ -182,7 +182,7 @@ it 'does not send the platform in the body' do expect(Docker.connection).to receive(:post) { |_path, _query, opts| - expect(MultiJson.load(opts[:body])).to_not have_key('platform') + expect(JSON.parse(opts[:body])).to_not have_key('platform') '{"Id":"deadbeef"}' } diff --git a/spec/docker/event_spec.rb b/spec/docker/event_spec.rb index 0ba6effe..24a25356 100644 --- a/spec/docker/event_spec.rb +++ b/spec/docker/event_spec.rb @@ -141,7 +141,7 @@ context 'with a new api response' do let(:event) do Docker::Event.new_event( - MultiJson.dump(api_response), + JSON.generate(api_response), nil, nil ) diff --git a/spec/docker/util_spec.rb b/spec/docker/util_spec.rb index 5fcbfb6b..a5e7edb3 100644 --- a/spec/docker/util_spec.rb +++ b/spec/docker/util_spec.rb @@ -239,7 +239,7 @@ def expect_tar_entries(*entries) :serveraddress => 'https://registry.com/' } } - let(:credential_string) { MultiJson.dump(credentials) } + let(:credential_string) { JSON.generate(credentials) } let(:encoded_creds) { Base64.urlsafe_encode64(credential_string) } let(:expected_header) { { @@ -280,7 +280,7 @@ def expect_tar_entries(*entries) } let(:credentials_object) do - MultiJson.dump( + JSON.generate( :'https://registry.com/' => { username: 'test', password: 'password', @@ -305,7 +305,7 @@ def expect_tar_entries(*entries) context 'given credentials as a String' do it 'returns an X-Registry-Config header encoded' do expect( - subject.build_config_header(MultiJson.dump(credentials)) + subject.build_config_header(JSON.generate(credentials)) ).to eq(expected_header) end end diff --git a/spec/docker_spec.rb b/spec/docker_spec.rb index b6656d13..7d92b485 100644 --- a/spec/docker_spec.rb +++ b/spec/docker_spec.rb @@ -218,7 +218,7 @@ it 'logs in and sets the creds' do skip_without_auth expect(authentication).to be true - expect(Docker.creds).to eq(MultiJson.dump(credentials)) + expect(Docker.creds).to eq(JSON.generate(credentials)) end end