diff --git a/CHANGELOG.md b/CHANGELOG.md index e38699fa..9eb1b4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.9.0 - 2018/11/29 + +* [FEATURE] Added a `render_as_json` API. Similar to `render_as_hash` but returns a JSONified hash. Please see pr [#119](https://github.com/procore/blueprinter/pull/119). Thanks to [@ritikesh](https://github.com/ritikesh). +* [FEATURE] Sorting of fields in the response is now configurable to sort by definition or by name(asc only). Please see pr [#119](https://github.com/procore/blueprinter/pull/119). Thanks to [@ritikesh](https://github.com/ritikesh). +* [ENHANCEMENT] Updated readme for above features and some existing undocumented features like `exclude fields`, `render_as_hash`. Please see pr [#119](https://github.com/procore/blueprinter/pull/119). Thanks to [@ritikesh](https://github.com/ritikesh). + ## 0.8.0 - 2018/11/19 * [FEATURE] Extend Support for other JSON encoders like yajl-ruby. Please see pr [#118](https://github.com/procore/blueprinter/pull/118). Thanks to [@ritikesh](https://github.com/ritikesh). diff --git a/README.md b/README.md index af9386ed..fe8533e1 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,40 @@ Output: } ``` +### Exclude fields +You can specifically choose to exclude certain fields for specific views +```ruby +class UserBlueprint < Blueprinter::Base + identifier :uuid + field :email, name: :login + + view :normal do + fields :first_name, :last_name + end + + view :extended do + include_view :normal + field :address + exclude :last_name + end +end +``` + +Usage: +```ruby +puts UserBlueprint.render(user, view: :extended) +``` + +Output: +```json +{ + "uuid": "733f0758-8f21-4719-875f-262c3ec743af", + "address": "123 Fake St.", + "first_name": "John", + "login": "john.doe@some.fake.email.domain" +} +``` + ### Associations You may include associated objects. Say for example, a user has projects: ```ruby @@ -278,6 +312,42 @@ Output: } ``` +### render_as_hash +Same as `render`, returns a Ruby Hash. + +Usage: + +```ruby +puts UserBlueprint.render_as_hash(user, company: company) +``` + +Output: + +```ruby +{ + uuid: "733f0758-8f21-4719-875f-262c3ec743af", + company_name: "My Company LLC" +} +``` + +### render_as_json +Same as `render`, returns a Ruby Hash JSONified. This will call JSONify all keys and values. + +Usage: + +```ruby +puts UserBlueprint.render_as_json(user, company: company) +``` + +Output: + +```ruby +{ + "uuid" => "733f0758-8f21-4719-875f-262c3ec743af", + "company_name" => "My Company LLC" +} +``` + ### Conditional field `field` supports `:if` and `:unless` options argument that can be used to serialize the field conditionally. @@ -328,6 +398,34 @@ $ gem install blueprinter You should also have `require 'json'` already in your project if you are not using Rails or if you are not using Oj. +## Sorting +By default the response sorts the keys by name. If you want the fields to be sorted in the order of definition, use the below configuration option. + +Usage: + +```ruby +Blueprinter.configure do |config| + config.sort_fields_by = :definition +end +``` + +```ruby +class UserBlueprint < Blueprinter::Base + identifier :name + field :email + field :birthday, datetime_format: "%m/%d/%Y" +end +``` + +Output: +```json +{ + "name": "John Doe", + "email": "john.doe@some.fake.email.domain", + "birthday": "03/04/1994" +} +``` + ## OJ By default, Blueprinter will be calling `JSON.generate(object)` internally and it expects that you have `require 'json'` already in your project's code. You may use `Oj` to generate in place of `JSON` like so: @@ -352,7 +450,7 @@ gem 'oj' [yajl-ruby](https://github.com/brianmario/yajl-ruby) is a fast and powerful JSON generator/parser. To use `yajl-ruby` in place of `JSON / OJ`, use: ```ruby -require 'yajl' # you can skip this if OJ has already been required. +require 'yajl' # you can skip this if yajl has already been required. Blueprinter.configure do |config| config.generator = Yajl::Encoder # default is JSON @@ -360,7 +458,7 @@ Blueprinter.configure do |config| end ``` -##### Note: You should be doing this only if you aren't using `yajl-ruby` through the JSON API by requiring `yajl/json_gem`. More details [here](https://github.com/brianmario/yajl-ruby#json-gem-compatibility-api). In this case, `JSON.generate` is patched to use `Yajl::Encoder.encode` internally. +Note: You should be doing this only if you aren't using `yajl-ruby` through the JSON API by requiring `yajl/json_gem`. More details [here](https://github.com/brianmario/yajl-ruby#json-gem-compatibility-api). In this case, `JSON.generate` is patched to use `Yajl::Encoder.encode` internally. ## How to Document diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index 5b8fe841..ff5cb41d 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -175,8 +175,7 @@ def self.association(method, options = {}, &block) # # @return [String] JSON formatted String def self.render(object, options = {}) - view_name = options.delete(:view) || :default - jsonify(prepare(object, view_name: view_name, local_options: options)) + jsonify(prepare_for_render(object, options)) end # Generates a hash. @@ -196,8 +195,27 @@ def self.render(object, options = {}) # # @return [Hash] def self.render_as_hash(object, options= {}) - view_name = options.delete(:view) || :default - prepare(object, view_name: view_name, local_options: options) + prepare_for_render(object, options) + end + + # Generates a JSONified hash. + # Takes a required object and an optional view. + # + # @param object [Object] the Object to serialize upon. + # @param options [Hash] the options hash which requires a :view. Any + # additional key value pairs will be exposed during serialization. + # @option options [Symbol] :view Defaults to :default. + # The view name that corresponds to the group of + # fields to be serialized. + # + # @example Generating a hash with an extended view + # post = Post.all + # Blueprinter::Base.render_as_json post, view: :extended + # # => [{"id" => "1", "title" => "Hello"},{"id" => "2", "title" => "My Day"}] + # + # @return [Hash] + def self.render_as_json(object, options= {}) + prepare_for_render(object, options).as_json end # This is the magic method that converts complex objects into a simple hash @@ -308,7 +326,12 @@ def self.view(view_name) @current_view = view_collection[:default] end - private + # Begin private class methods + def self.prepare_for_render(object, options) + view_name = options.delete(:view) || :default + prepare(object, view_name: view_name, local_options: options) + end + private_class_method :prepare_for_render def self.inherited(subclass) subclass.send(:view_collection).inherit(view_collection) diff --git a/lib/blueprinter/configuration.rb b/lib/blueprinter/configuration.rb index 7914fcdd..3fbcc573 100644 --- a/lib/blueprinter/configuration.rb +++ b/lib/blueprinter/configuration.rb @@ -1,10 +1,11 @@ module Blueprinter class Configuration - attr_accessor :generator, :method + attr_accessor :generator, :method, :sort_fields_by def initialize @generator = JSON @method = :generate + @sort_fields_by = :name_asc end def jsonify(blob) diff --git a/lib/blueprinter/version.rb b/lib/blueprinter/version.rb index de2ff96b..f82a0117 100644 --- a/lib/blueprinter/version.rb +++ b/lib/blueprinter/version.rb @@ -1,3 +1,3 @@ module Blueprinter - VERSION = '0.8.0' + VERSION = '0.9.0' end diff --git a/lib/blueprinter/view_collection.rb b/lib/blueprinter/view_collection.rb index 1fd49636..0193b69c 100644 --- a/lib/blueprinter/view_collection.rb +++ b/lib/blueprinter/view_collection.rb @@ -1,12 +1,13 @@ module Blueprinter # @api private class ViewCollection - attr_reader :views + attr_reader :views, :sort_by_definition def initialize @views = { identifier: View.new(:identifier), default: View.new(:default) } + @sort_by_definition = Blueprinter.configuration.sort_fields_by.eql?(:definition) end def inherit(view_collection) @@ -20,7 +21,9 @@ def has_view?(view_name) end def fields_for(view_name) - identifier_fields + sortable_fields(view_name).values.sort_by(&:name) + fields = sortable_fields(view_name).values + sorted_fields = sort_by_definition ? fields : fields.sort_by(&:name) + identifier_fields + sorted_fields end def [](view_name) @@ -35,10 +38,10 @@ def identifier_fields def sortable_fields(view_name) fields = views[:default].fields - fields = fields.merge(views[view_name].fields) + fields = merge_fields(fields, views[view_name].fields) views[view_name].included_view_names.each do |included_view_name| if view_name != included_view_name - fields = fields.merge(sortable_fields(included_view_name)) + fields = merge_fields(fields, sortable_fields(included_view_name)) end end @@ -48,5 +51,13 @@ def sortable_fields(view_name) fields end + + def merge_fields(source_fields, included_fields) + if sort_by_definition + included_fields.merge(source_fields) + else + source_fields.merge(included_fields) + end + end end end diff --git a/spec/integrations/base_spec.rb b/spec/integrations/base_spec.rb index 46e1a2fe..98102d53 100644 --- a/spec/integrations/base_spec.rb +++ b/spec/integrations/base_spec.rb @@ -158,6 +158,18 @@ end end end + + describe '::render_as_json' do + subject { blueprint_with_block.render_as_json(object_with_attributes) } + context 'Outside Rails project' do + context 'Given passed object has dot notation accessible attributes' do + let(:obj) { object_with_attributes } + it 'returns a hash with expected format' do + expect(subject).to eq({ "id" => obj.id, "position_and_company" => "#{obj.position} at #{obj.company}"}) + end + end + end + end describe 'identifier' do let(:rendered) do diff --git a/spec/units/configuration_spec.rb b/spec/units/configuration_spec.rb index cbaca611..f41b202a 100644 --- a/spec/units/configuration_spec.rb +++ b/spec/units/configuration_spec.rb @@ -7,6 +7,7 @@ after { Blueprinter.configure { |config| config.generator = JSON config.method = :generate + config.sort_fields_by = :name_asc } } it 'should set the generator' do @@ -22,5 +23,12 @@ expect(Blueprinter.configuration.generator).to be(Yajl::Encoder) expect(Blueprinter.configuration.method).to be(:encode) end + + it 'should set the sort_fields_by' do + Blueprinter.configure { |config| + config.sort_fields_by = :definition + } + expect(Blueprinter.configuration.sort_fields_by).to be(:definition) + end end end