diff --git a/Gemfile.lock b/Gemfile.lock index b9f6b37..28775bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - decanter (5.0.0) + decanter (5.1.0) actionpack (>= 7.1.3.2) activesupport rails (>= 7.1.3.2) diff --git a/README.md b/README.md index 318178a..7c50a80 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,31 @@ gem 'decanter', '~> 5.0' ## Contents -- [Basic Usage](#basic-usage) - - [Decanters](#decanters) - - [Generators](#generators) - - [Decanting Collections](#decanting-collections) - - [Nested resources](#nested-resources) - - [Default parsers](#default-parsers) - - [Parser options](#parser-options) - - [Exceptions](#exceptions) -- [Advanced usage](#advanced-usage) - - [Custom parsers](#custom-parsers) - - [Squashing inputs](#squashing-inputs) - - [Chaining parsers](#chaining-parsers) - - [Requiring params](#requiring-params) - - [Global configuration](#global-configuration) -- [Contributing](#contributing) +- [Decanter](#decanter) + - [Migration Guides](#migration-guides) + - [Contents](#contents) + - [Basic Usage](#basic-usage) + - [Decanters](#decanters) + - [Generators](#generators) + - [Decanters](#decanters-1) + - [Parsers](#parsers) + - [Resources](#resources) + - [Decanting Collections](#decanting-collections) + - [Control Over Decanting Collections](#control-over-decanting-collections) + - [Nested resources](#nested-resources) + - [Default parsers](#default-parsers) + - [Parser options](#parser-options) + - [Exceptions](#exceptions) + - [Advanced Usage](#advanced-usage) + - [Custom Parsers](#custom-parsers) + - [Custom parser methods](#custom-parser-methods) + - [Custom parser base classes](#custom-parser-base-classes) + - [Squashing inputs](#squashing-inputs) + - [Chaining parsers](#chaining-parsers) + - [Requiring params](#requiring-params) + - [Default values](#default-values) + - [Global configuration](#global-configuration) + - [Contributing](#contributing) ## Basic Usage @@ -163,6 +173,7 @@ Decanter comes with the following parsers out of the box: - `:phone` - `:string` - `:array` +- `:json` Note: these parsers are designed to operate on a single value, except for `:array`. This parser expects an array, and will use the `parse_each` option to call a given parser on each of its elements: @@ -170,6 +181,11 @@ Note: these parsers are designed to operate on a single value, except for `:arra input :ids, :array, parse_each: :integer ``` +The `:json` parser may also accept an array, but the array must be provided as a single JSON-encoded string value: +``` +'["abc", "def"]' +``` + ### Parser options Some parsers can receive options that modify their behavior. These options are passed in as named arguments to `input`: diff --git a/lib/decanter/parser/json_parser.rb b/lib/decanter/parser/json_parser.rb new file mode 100644 index 0000000..ed7f050 --- /dev/null +++ b/lib/decanter/parser/json_parser.rb @@ -0,0 +1,20 @@ +module Decanter + module Parser + class JsonParser < ValueParser + + parser do |val, options| + next if val.nil? || val === '' + raise Decanter::ParseError.new 'Expects a JSON string' unless val.is_a?(String) + parse_json(val) + end + + def self.parse_json(val) + begin + JSON.parse(val) + rescue + raise Decanter::ParseError.new 'Invalid JSON string' + end + end + end + end +end diff --git a/lib/decanter/version.rb b/lib/decanter/version.rb index 7a560e6..e12ba21 100644 --- a/lib/decanter/version.rb +++ b/lib/decanter/version.rb @@ -1,3 +1,3 @@ module Decanter - VERSION = '5.0.0'.freeze + VERSION = '5.1.0'.freeze end diff --git a/spec/decanter/parser/json_parser_spec.rb b/spec/decanter/parser/json_parser_spec.rb new file mode 100644 index 0000000..eb66c83 --- /dev/null +++ b/spec/decanter/parser/json_parser_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe 'JsonParser' do + + let(:name) { :foo } + + let(:parser) { Decanter::Parser::JsonParser } + + describe '#parse' do + + context 'with a valid JSON string' do + it 'parses the string value and returns a parsed JSON' do + expect(parser.parse(name, '{"key": "value"}')).to match({name => {"key" => "value"}}) + expect(parser.parse(name, '["hello", "goodbye"]')).to match({name => ["hello", "goodbye"]}) + end + end + + context 'with empty string' do + it 'returns nil' do + expect(parser.parse(name, '')).to match({name => nil}) + end + end + + context 'with nil' do + it 'returns nil' do + expect(parser.parse(name, nil)).to match({name => nil}) + end + end + + context 'with a non-string value' do + it 'raises a Decanter::ParseError' do + expect { parser.parse(name, 1) }.to raise_error(Decanter::ParseError, 'Expects a JSON string') + expect { parser.parse(name, true) }.to raise_error(Decanter::ParseError, 'Expects a JSON string') + expect { parser.parse(name, {}) }.to raise_error(Decanter::ParseError, 'Expects a JSON string') + end + end + + context 'with a string that is invalid JSON' do + json_parser_error = 'Invalid JSON string' + it 'raises a Decanter::ParseError' do + expect { parser.parse(name, 'invalid') }.to raise_error(Decanter::ParseError, json_parser_error) + expect { parser.parse(name, '{ name: "John Smith", age: 30 }') }.to raise_error(Decanter::ParseError, json_parser_error) + expect { parser.parse(name, '{\"bio\": \"Line1 \n Line2\"}') }.to raise_error(Decanter::ParseError, json_parser_error) + expect { parser.parse(name, '{ "name": "John Smith", "age": 30, }') }.to raise_error(Decanter::ParseError, json_parser_error) + end + end + end +end