Skip to content

Commit bf743b5

Browse files
committed
Don't JSON encode decimals as string
- Use Oj consistently for encoding and decoding. - Remove custom encoder in JSON initializer. - Configure Oj to encode BigDecimals as JSON numbers. - Override BigDecimal's 'as_json' method; it's called by ActiveSupport and returns a string by default (before invoking the actual JSON encoder). - Adapt rescued error classes. - Add specs to test JSON encoding/decoding for service instance parameters (create, update, get) and service binding parameters (create, get).
1 parent a9b49cf commit bf743b5

File tree

7 files changed

+256
-38
lines changed

7 files changed

+256
-38
lines changed

app/models/runtime/droplet_model.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def docker_user
133133
begin
134134
docker_exec_metadata = Oj.load(execution_metadata)
135135
container_user = docker_exec_metadata['user']
136-
rescue EncodingError
136+
rescue JSON::ParserError
137137
# ignore
138138
end
139139
end

app/presenters/v3/base_presenter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ def initialize(resource, show_secrets: true, censored_message: Censorship::PRIVA
1313
@decorators = decorators
1414
end
1515

16+
def as_json(*_args)
17+
to_hash
18+
end
19+
1620
private
1721

1822
def redact(unredacted_value)

config/initializers/json.rb

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1-
require 'active_support/json/encoding'
1+
require 'bigdecimal'
2+
3+
# Override BigDecimal's 'as_json' to return self so that Oj can handle it according to its configuration.
4+
# By default, ActiveSupport encodes BigDecimal as a string which we do not want.
5+
class BigDecimal
6+
def as_json(*_args)
7+
self
8+
end
9+
end
210

311
module CCInitializers
412
def self.json(_cc_config)
5-
Oj::Rails.optimize # Use optimized encoders instead of as_json() methods for available classes.
613
Oj.default_options = {
7-
bigdecimal_load: :bigdecimal,
8-
mode: :rails
9-
}
10-
11-
ActiveSupport.json_encoder = Class.new do
12-
attr_reader :options
13-
14-
def initialize(options=nil)
15-
@options = options || {}
16-
end
17-
18-
def encode(value)
19-
v = if value.is_a?(VCAP::CloudController::Presenters::V3::BasePresenter)
20-
value.to_hash
21-
else
22-
value.as_json(options.dup)
23-
end
14+
time_format: :ruby, # Encode Time/DateTime in Ruby-style string format
15+
mode: :rails, # Rails-compatible JSON behavior
16+
bigdecimal_load: :bigdecimal, # Decode JSON decimals as BigDecimal
17+
compat_bigdecimal: true, # Required in :rails mode to avoid Float decoding
18+
bigdecimal_as_decimal: true # Encode BigDecimal as JSON number (not string)
19+
}.freeze
2420

25-
if Rails.env.test?
26-
Oj.dump(v, time_format: :ruby)
27-
else
28-
Oj.dump(v, options.merge(time_format: :ruby))
29-
end
30-
end
31-
end
21+
Oj.optimize_rails # Use Oj for Rails JSON encoding/decoding
3222
end
3323
end

lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def config_path_for(resource_type)
6565

6666
def fetch_json(path)
6767
Oj.load(File.read(path))
68-
rescue Oj::ParseError, EncodingError => e
68+
rescue JSON::ParserError, EncodingError => e
6969
raise BlobstoreError.new("Failed to parse storage-cli JSON at #{path}: #{e.message}")
7070
end
7171

spec/request/service_credential_bindings_spec.rb

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@
1414
space.add_developer(user)
1515
headers_for(user)
1616
end
17+
let(:parameters_mixed_data_types_as_json_string) do
18+
'{"boolean":true,"string":"a string","int":123,"float":3.14159,"optional":null,"object":{"a":"b"},"array":["c","d"]}'
19+
end
20+
let(:parameters_mixed_data_types_as_hash) do
21+
{
22+
boolean: true,
23+
string: 'a string',
24+
int: 123,
25+
float: 3.14159,
26+
optional: nil,
27+
object: { a: 'b' },
28+
array: %w[c d]
29+
}
30+
end
1731

1832
describe 'GET /v3/service_credential_bindings' do
1933
it_behaves_like 'list query endpoint' do
@@ -1072,11 +1086,7 @@ def check_filtered_bindings(*bindings)
10721086
end
10731087
end
10741088

1075-
context 'when the broker will returns params' do
1076-
before do
1077-
stub_param_broker_request_for_binding(binding, binding_params)
1078-
end
1079-
1089+
context 'when the broker returns params' do
10801090
it 'sends a request to the broker including the identity header' do
10811091
api_call.call(headers_for(user, scopes: %w[cloud_controller.admin]))
10821092

@@ -1096,6 +1106,24 @@ def check_filtered_bindings(*bindings)
10961106
expect(last_response).to have_status_code(200)
10971107
expect(parsed_response).to eq(binding_params)
10981108
end
1109+
1110+
context 'when the broker returns params with mixed data types' do
1111+
before do
1112+
stub_param_broker_request_for_binding_with_json_string(binding, parameters_mixed_data_types_as_json_string)
1113+
end
1114+
1115+
it 'correctly parses all data types and returns the desired JSON string' do
1116+
allow_any_instance_of(VCAP::CloudController::ServiceBindingRead).to receive(:fetch_parameters).and_wrap_original do |m, instance|
1117+
result = m.call(instance)
1118+
expect(result).to eq(parameters_mixed_data_types_as_hash) # correct internal representation
1119+
result
1120+
end
1121+
1122+
api_call.call(admin_headers)
1123+
expect(last_response).to have_status_code(200)
1124+
expect(last_response).to match(/#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/)
1125+
end
1126+
end
10991127
end
11001128
end
11011129
end
@@ -1134,11 +1162,31 @@ def check_filtered_bindings(*bindings)
11341162
).to have_been_made.once
11351163
end
11361164

1137-
it 'returns the params in the response body' do
1138-
api_call.call(admin_headers)
1165+
context 'when the broker returns params' do
1166+
it 'returns the params in the response body' do
1167+
api_call.call(admin_headers)
11391168

1140-
expect(last_response).to have_status_code(200)
1141-
expect(parsed_response).to eq(binding_params)
1169+
expect(last_response).to have_status_code(200)
1170+
expect(parsed_response).to eq(binding_params)
1171+
end
1172+
1173+
context 'when the broker returns params with mixed data types' do
1174+
before do
1175+
stub_param_broker_request_for_binding_with_json_string(binding, parameters_mixed_data_types_as_json_string)
1176+
end
1177+
1178+
it 'correctly parses all data types and returns the desired JSON string' do
1179+
allow_any_instance_of(VCAP::CloudController::ServiceBindingRead).to receive(:fetch_parameters).and_wrap_original do |m, instance|
1180+
result = m.call(instance)
1181+
expect(result).to eq(parameters_mixed_data_types_as_hash) # correct internal representation
1182+
result
1183+
end
1184+
1185+
api_call.call(admin_headers)
1186+
expect(last_response).to have_status_code(200)
1187+
expect(last_response).to match(/#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/)
1188+
end
1189+
end
11421190
end
11431191

11441192
context "last service key operation is in 'create succeeded' state" do
@@ -1643,6 +1691,28 @@ def check_filtered_bindings(*bindings)
16431691
expect(service_instance.service_bindings.count).to eq(0)
16441692
end
16451693
end
1694+
1695+
context 'when providing parameters with mixed data types' do
1696+
let(:request_body) do
1697+
"{\"type\":\"app\",\"name\":\"#{binding_name}\"," \
1698+
"\"relationships\":{\"service_instance\":{\"data\":{\"guid\":\"#{service_instance_guid}\"}},\"app\":{\"data\":{\"guid\":\"#{app_guid}\"}}}," \
1699+
"\"parameters\":#{parameters_mixed_data_types_as_json_string}}"
1700+
end
1701+
1702+
it 'correctly parses all data types and sends the desired JSON string to the service broker' do
1703+
post '/v3/service_credential_bindings', request_body, admin_headers
1704+
1705+
expect_any_instance_of(VCAP::Services::ServiceBrokers::V2::Client).to receive(:bind).
1706+
with(binding, hash_including(arbitrary_parameters: parameters_mixed_data_types_as_hash)). # correct internal representation
1707+
and_call_original
1708+
1709+
stub_request(:put, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance_guid}/service_bindings/#{binding.guid}").
1710+
with(query: { accepts_incomplete: true }, body: /"parameters":#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/).
1711+
to_return(status: 201, body: '{}')
1712+
1713+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1714+
end
1715+
end
16461716
end
16471717
end
16481718

@@ -1877,6 +1947,29 @@ def check_filtered_bindings(*bindings)
18771947
expect(service_instance.service_keys.count).to eq(0)
18781948
end
18791949
end
1950+
1951+
context 'when providing parameters with mixed data types' do
1952+
let(:key) { VCAP::CloudController::ServiceKey.last }
1953+
let(:request_body) do
1954+
"{\"type\":\"key\",\"name\":\"#{binding_name}\"," \
1955+
"\"relationships\":{\"service_instance\":{\"data\":{\"guid\":\"#{service_instance_guid}\"}}}," \
1956+
"\"parameters\":#{parameters_mixed_data_types_as_json_string}}"
1957+
end
1958+
1959+
it 'correctly parses all data types and sends the desired JSON string to the service broker' do
1960+
post '/v3/service_credential_bindings', request_body, admin_headers
1961+
1962+
expect_any_instance_of(VCAP::Services::ServiceBrokers::V2::Client).to receive(:bind).
1963+
with(key, hash_including(arbitrary_parameters: parameters_mixed_data_types_as_hash)). # correct internal representation
1964+
and_call_original
1965+
1966+
stub_request(:put, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance_guid}/service_bindings/#{key.guid}").
1967+
with(query: { accepts_incomplete: true }, body: /"parameters":#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/).
1968+
to_return(status: 201, body: '{}')
1969+
1970+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1971+
end
1972+
end
18801973
end
18811974
end
18821975

@@ -2585,11 +2678,15 @@ def operate_on(binding)
25852678
end
25862679

25872680
def stub_param_broker_request_for_binding(binding, binding_params, status: 200)
2681+
stub_param_broker_request_for_binding_with_json_string(binding, binding_params.to_json, status:)
2682+
end
2683+
2684+
def stub_param_broker_request_for_binding_with_json_string(binding, binding_params_as_json_string, status: 200)
25882685
instance = binding.service_instance
25892686
broker_url = instance.service_broker.broker_url
25902687
broker_binding_url = "#{broker_url}/v2/service_instances/#{instance.guid}/service_bindings/#{binding.guid}"
25912688

25922689
stub_request(:get, /#{broker_binding_url}/).
2593-
to_return(status: status, body: { parameters: binding_params }.to_json)
2690+
to_return(status: status, body: "{\"parameters\":#{binding_params_as_json_string}}")
25942691
end
25952692
end

spec/request/service_instances_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@
88
let(:space) { VCAP::CloudController::Space.make(organization: org, created_at: Time.now.utc - 1.second) }
99
let!(:space_annotation) { VCAP::CloudController::SpaceAnnotationModel.make(key_prefix: 'pre.fix', key_name: 'baz', value: 'wow', space: space) }
1010
let(:another_space) { VCAP::CloudController::Space.make }
11+
let(:parameters_mixed_data_types_as_json_string) do
12+
'{"boolean":true,"string":"a string","int":123,"float":3.14159,"optional":null,"object":{"a":"b"},"array":["c","d"]}'
13+
end
14+
let(:parameters_mixed_data_types_as_hash) do
15+
{
16+
boolean: true,
17+
string: 'a string',
18+
int: 123,
19+
float: 3.14159,
20+
optional: nil,
21+
object: { a: 'b' },
22+
array: %w[c d]
23+
}
24+
end
1125

1226
describe 'GET /v3/service_instances/:guid' do
1327
let(:api_call) { ->(user_headers) { get "/v3/service_instances/#{guid}", nil, user_headers } }
@@ -605,6 +619,22 @@ def check_filtered_instances(*instances)
605619
end
606620
end
607621

622+
context 'when the service broker returns parameters with mixed data types' do
623+
let(:body) { "{\"parameters\":#{parameters_mixed_data_types_as_json_string}}" }
624+
625+
it 'correctly parses all data types and returns the desired JSON string' do
626+
allow_any_instance_of(VCAP::CloudController::ServiceInstanceRead).to receive(:fetch_parameters).and_wrap_original do |m, instance|
627+
result = m.call(instance)
628+
expect(result).to eq(parameters_mixed_data_types_as_hash) # correct internal representation
629+
result
630+
end
631+
632+
get "/v3/service_instances/#{instance.guid}/parameters", nil, admin_headers
633+
expect(last_response).to have_status_code(200)
634+
expect(last_response).to match(/#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/)
635+
end
636+
end
637+
608638
it 'sends the correct request to the service broker' do
609639
get "/v3/service_instances/#{instance.guid}/parameters", nil, headers_for(user, scopes: %w[cloud_controller.admin])
610640

@@ -1087,6 +1117,28 @@ def check_filtered_instances(*instances)
10871117
allow(Steno).to receive(:logger).with('cc.api').and_return(mock_logger)
10881118
end
10891119

1120+
context 'when providing parameters with mixed data types' do
1121+
let(:request_body) do
1122+
"{\"type\":\"managed\",\"name\":\"#{name}\"," \
1123+
"\"relationships\":{\"space\":{\"data\":{\"guid\":\"#{space_guid}\"}},\"service_plan\":{\"data\":{\"guid\":\"#{service_plan_guid}\"}}}," \
1124+
"\"parameters\":#{parameters_mixed_data_types_as_json_string}}"
1125+
end
1126+
1127+
it 'correctly parses all data types and sends the desired JSON string to the service broker' do
1128+
post '/v3/service_instances', request_body, space_dev_headers
1129+
1130+
expect_any_instance_of(VCAP::Services::ServiceBrokers::V2::Client).to receive(:provision).
1131+
with(instance, hash_including(arbitrary_parameters: parameters_mixed_data_types_as_hash)). # correct internal representation
1132+
and_call_original
1133+
1134+
stub_request(:put, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
1135+
with(query: { 'accepts_incomplete' => true }, body: /"parameters":#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/).
1136+
to_return(status: 201, body: '{}')
1137+
1138+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1139+
end
1140+
end
1141+
10901142
it 'creates a service instance in the database' do
10911143
api_call.call(space_dev_headers)
10921144

@@ -1871,6 +1923,27 @@ def check_filtered_instances(*instances)
18711923
allow(Steno).to receive(:logger).with('cc.api').and_return(mock_logger)
18721924
end
18731925

1926+
context 'when providing parameters with mixed data types' do
1927+
let(:request_body) do
1928+
"{\"parameters\":#{parameters_mixed_data_types_as_json_string}}"
1929+
end
1930+
let(:instance) { VCAP::CloudController::ServiceInstance.last }
1931+
1932+
it 'correctly parses all data types and sends the desired JSON string to the service broker' do
1933+
patch "/v3/service_instances/#{guid}", request_body, space_dev_headers
1934+
1935+
expect_any_instance_of(VCAP::Services::ServiceBrokers::V2::Client).to receive(:update).
1936+
with(instance, instance.service_plan, hash_including(arbitrary_parameters: parameters_mixed_data_types_as_hash)). # correct internal representation
1937+
and_call_original
1938+
1939+
stub_request(:patch, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
1940+
with(query: { 'accepts_incomplete' => true }, body: /"parameters":#{Regexp.escape(parameters_mixed_data_types_as_json_string)}/).
1941+
to_return(status: 200, body: '{}')
1942+
1943+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1944+
end
1945+
end
1946+
18741947
it 'responds with a pollable job' do
18751948
api_call.call(space_dev_headers)
18761949

0 commit comments

Comments
 (0)