Skip to content

Instantly share code, notes, and snippets.

@timnew
Last active August 2, 2018 07:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timnew/3091503e51df34d1b2339f32a8a04a6c to your computer and use it in GitHub Desktop.
Save timnew/3091503e51df34d1b2339f32a8a04a6c to your computer and use it in GitHub Desktop.
module JsonConvertible
extend ActiveSupport::Concern
included do
unless instance_methods(true).include?(:members) # Support Struct
def self.members(*attributes)
@members = [] if @members.nil?
unless attributes.empty?
attr_accessor(*attributes)
@members.push(*attributes)
end
@members
end
def members
self.class.members
end
end
trim_nil_member(false)
end
class_methods do
# TODO: Maybe should rename to from_h
def from_json(hash, &block)
new.load_json(hash, &block)
end
def trim_nil_member(value = true)
if value
# TODO: Maybe should rename to to_h
alias_method :to_json, :to_json_without_nil_member
else
# TODO: Maybe should rename to to_h
alias_method :to_json, :to_json_with_nil_member
end
end
end
# TODO: Maybe should rename to load_h or load_values
def load_json(hash)
hash.each do |k, v|
key = :"#{k.to_s.underscore}="
send(key, v) if respond_to?(key)
end
yield(self) if block_given?
self
end
# TODO: Maybe should rename to to_h
def to_json_with_nil_member
members.each_with_object({}) do |member, json|
json[member.to_s.camelize(:lower).to_sym] = send(member)
end
end
# TODO: Maybe should rename to to_h_compacted
def to_json_without_nil_member
to_json_with_nil_member.try(:compact)
end
end
RSpec.describe JsonConvertible do
let(:extended_json) do
{
fieldNotExist: false,
coolField: 'cool_field',
anotherField: 'another_field',
awesomeField: 'awesome_field'
}
end
let(:json) do
{
coolField: 'cool_field',
anotherField: 'another_field',
awesomeField: 'awesome_field'
}
end
context 'used in Struct' do
TestStruct = Struct.new(:cool_field, :another_field, :awesome_field) do
include JsonConvertible
def not_a_field
'not_a_field'
end
end
describe 'extended' do
describe 'class behaviours' do
subject { TestStruct }
it { is_expected.to respond_to(:from_json) }
it { is_expected.to respond_to(:members) }
its(:members) { is_expected.to eq [:cool_field, :another_field, :awesome_field] }
end
describe 'instance_behaviours' do
subject { TestStruct.new }
it { is_expected.to respond_to(:to_json) }
it { is_expected.to respond_to(:members) }
it { is_expected.to respond_to(:load_json) }
its(:members) { is_expected.to eq [:cool_field, :another_field, :awesome_field] }
end
end
describe '.from_json' do
subject { TestStruct.from_json(json) }
it 'should be instantiated from json' do
is_expected.to have_attributes cool_field: 'cool_field',
another_field: 'another_field',
awesome_field: 'awesome_field'
end
it 'should serialised to json' do
expect(subject.to_json).to eq json
end
it 'should not complain for unknown field' do
TestStruct.from_json(extended_json)
end
it 'shoudl invoke block if given' do
expect { |b| TestStruct.from_json(json, &b) }.to yield_with_args
end
end
describe '#load_json' do
subject { TestStruct.new.load_json(json) }
it 'should be instantiated from json' do
is_expected.to have_attributes cool_field: 'cool_field',
another_field: 'another_field',
awesome_field: 'awesome_field'
end
it 'should serialised to json' do
expect(subject.to_json).to eq json
end
it 'should not complain for unknown field' do
TestStruct.new.load_json(extended_json)
end
it 'shoudl invoke block if given' do
model = TestStruct.new
expect { |b| model.load_json(json, &b) }.to yield_with_args(model)
end
end
end
context 'used in Class' do
class TestModel
include JsonConvertible
attr_accessor :cool_field
members :cool_field, :another_field, :awesome_field
attr_accessor :another_field
def not_a_field
'not_a_field'
end
end
describe 'extended' do
describe 'class behaviours' do
subject { TestModel }
it { is_expected.to respond_to(:from_json) }
it { is_expected.to respond_to(:members) }
its(:members) { is_expected.to eq [:cool_field, :another_field, :awesome_field] }
end
describe 'instance_behaviours' do
subject { TestModel.new }
it { is_expected.to respond_to(:to_json) }
it { is_expected.to respond_to(:members) }
it { is_expected.to respond_to(:load_json) }
its(:members) { is_expected.to eq [:cool_field, :another_field, :awesome_field] }
end
end
describe '.from_json' do
subject { TestModel.from_json(json) }
it 'should be instantiated from json' do
is_expected.to have_attributes cool_field: 'cool_field',
another_field: 'another_field',
awesome_field: 'awesome_field'
end
it 'should serialised to json' do
expect(subject.to_json).to eq json
end
it 'should not complain for unknown field' do
TestModel.from_json(extended_json)
end
it 'shoudl invoke block if given' do
expect { |b| TestModel.from_json(json, &b) }.to yield_with_args
end
end
end
describe '#load_json' do
subject { TestModel.new.load_json(json) }
it 'should be instantiated from json' do
is_expected.to have_attributes cool_field: 'cool_field',
another_field: 'another_field',
awesome_field: 'awesome_field'
end
it 'should serialised to json' do
expect(subject.to_json).to eq json
end
it 'should not complain for unknown field' do
TestModel.new.load_json(extended_json)
end
it 'shoudl invoke block if given' do
model = TestModel.new
expect { |b| model.load_json(json, &b) }.to yield_with_args(model)
end
end
describe 'nil members handling' do
subject { model_class.from_json(field: 'value', empty: nil) }
context 'preserve nil members' do
let(:model_class) do
Struct.new(:field, :empty) do
include JsonConvertible
end
end
it { is_expected.to respond_to :to_json_with_nil_member }
it { is_expected.to respond_to :to_json_without_nil_member }
it { is_expected.to respond_to :to_json }
its(:to_json) { is_expected.to eq(field: 'value', empty: nil) }
end
context 'trim nil members' do
let(:model_class) do
Struct.new(:field, :empty) do
include JsonConvertible
trim_nil_member
end
end
it { is_expected.to respond_to :to_json_with_nil_member }
it { is_expected.to respond_to :to_json_without_nil_member }
it { is_expected.to respond_to :to_json }
its(:to_json) { is_expected.to eq(field: 'value') }
end
context 'reserve original #to_json' do
let(:model_class) do
Struct.new(:field, :empty) do
include JsonConvertible
def to_json
{
custom: true
}
end
end
end
it { is_expected.to respond_to :to_json_with_nil_member }
it { is_expected.to respond_to :to_json_without_nil_member }
it { is_expected.to respond_to :to_json }
its(:to_json) { is_expected.to eq(custom: true) }
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment