Skip to content

Commit 64a3681

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). - Add specs to test JSON encoding/decoding for service instance parameters (create, update, get).
1 parent a9b49cf commit 64a3681

File tree

3 files changed

+93
-26
lines changed

3 files changed

+93
-26
lines changed

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

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)