Skip to content

Commit

Permalink
Define entity in serializer for grape_swagger (#9)
Browse files Browse the repository at this point in the history
* Model entity in serializer definition

* Use constantize for ref

* Fix loading and entity camel_lower

* Update README with example for entity

* Change check for enumarable

* Revert using hash for attributes_to_serialize

* Rename method

* Add changelog entry
  • Loading branch information
Bhacaz authored Jul 17, 2020
1 parent 511eaef commit c264c16
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Add RubyGems version badge
* Handle inherit from a parent serializer
* Define entity in serializer for grape_swagger (#9)

## 0.1.1 (2020-07-13)

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,27 @@ class AccountSerializer
end
```

### Entity

You can define the entity of your serializer to generate documentation with the option `entity`.
The feature was build to work with [grape-swagger](https://github.com/ruby-grape/grape-swagger).
For more information about defining a model entity see the [Swagger documentation](https://swagger.io/specification/v2/?sbsearch=array%20response#schema-object).

```ruby
class AccountSerializer
include BrightSerializer::Serializer
attribute :id, entity: { type: :string, description: 'The id of the account' }
attribute :name

attribute :friends,
entity: {
type: :array, items: { ref: 'FriendSerializer' }, description: 'The list the account friends.'
} do |object|
FriendSerializer.new(object.friends)
end
end
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
7 changes: 5 additions & 2 deletions lib/bright_serializer/attribute.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# frozen_string_literal: true

require_relative 'entity/base'

module BrightSerializer
class Attribute
attr_reader :key, :block, :condition
attr_reader :key, :block, :condition, :entity
attr_accessor :transformed_key

def initialize(key, condition, &block)
def initialize(key, condition, entity, &block)
@key = key
@condition = condition
@block = block
@entity = entity ? Entity::Base.new(entity) : nil
end

def serialize(object, params)
Expand Down
42 changes: 42 additions & 0 deletions lib/bright_serializer/entity/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require_relative 'parser'

module BrightSerializer
module Entity
class Base
DEFAULT_DEFINITION = { type: :undefined }.freeze

# https://swagger.io/specification/v2/?sbsearch=array%20response#schema-object

def initialize(definition)
@definition = definition
end

def to_h
@definition.transform_keys! { |k| Inflector.camel_lower k.to_s }
parse_ref!
@definition
end

def parse_ref!
object = nested_hash(@definition, 'ref')
return unless object

ref_entity_name = Inflector.constantize(object.delete('ref')).entity_name
relation = "#/definitions/#{ref_entity_name}"
object['$ref'] = relation
end

def nested_hash(obj, key)
if obj.respond_to?(:key?) && obj.key?(key)
obj
elsif obj.respond_to?(:each)
r = nil
obj.find { |*a| r = nested_hash(a.last, key) }
r
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/bright_serializer/entity/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module BrightSerializer
module Entity
class Parser
attr_reader :model
attr_reader :endpoint

def initialize(model, endpoint)
@model = model
@endpoint = endpoint
end

def call
@model.entity
end
end
end
end
31 changes: 31 additions & 0 deletions lib/bright_serializer/inflector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,36 @@ def dash(underscored_word)
underscored_word.tr!('_', '-')
underscored_word
end

# File activesupport/lib/active_support/inflector/methods.rb, line 271
def constantize(camel_cased_word)
names = camel_cased_word.split('::')

# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?

# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?

names.inject(Object) do |constant, name|
if constant == Object
constant.const_get(name)
else
candidate = constant.const_get(name)
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)

# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.each_with_object(constant) do |ancestor, const|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
end

# owner is in Object, so raise
constant.const_get(name, false)
end
end
end
end
end
18 changes: 16 additions & 2 deletions lib/bright_serializer/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'set'
require_relative 'attribute'
require_relative 'inflector'
require_relative 'entity/base'

module BrightSerializer
module Serializer
Expand Down Expand Up @@ -31,7 +32,7 @@ def serialize(object)
end

def serializable_hash
if @object.respond_to?(:size) && !@object.respond_to?(:each_pair)
if @object.respond_to?(:each) && !@object.respond_to?(:each_pair)
@object.map { |o| serialize o }
else
serialize(@object)
Expand All @@ -57,7 +58,7 @@ def inherited(subclass)

def attributes(*attributes, **options, &block)
attributes.each do |key|
attribute = Attribute.new(key, options[:if], &block)
attribute = Attribute.new(key, options[:if], options[:entity], &block)
attribute.transformed_key = run_transform_key(key)
@attributes_to_serialize << attribute
end
Expand All @@ -80,6 +81,19 @@ def run_transform_key(input)
input.to_sym
end
end

def entity
{}.tap do |result|
@attributes_to_serialize.each do |attribute|
entity_value = attribute.entity&.to_h || BrightSerializer::Entity::Base::DEFAULT_DEFINITION
result.merge!(attribute.transformed_key => entity_value)
end
end
end

def entity_name
name.split('::').last.downcase
end
end
end
end
56 changes: 56 additions & 0 deletions spec/lib/bright_serializer/entity/base_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

RSpec.describe BrightSerializer::Entity::Base do
let(:instance) { described_class.new(type: :string) }
let(:entity_class) do
Class.new do
include BrightSerializer::Serializer

def self.entity_name
'user'
end
end
end

before do
allow(Inflector).to receive(:constantize) { entity_class }
end

describe '#to_h' do
subject { instance.to_h }

it 'return the definition' do
expect(subject).to eq('type' => :string)
end
end

describe '#parser_ref!' do
subject { instance.to_h }

let(:instance) { described_class.new(ref: 'SomeModule::User') }

it 'modified @definition' do
expect(subject).to eq('$ref' => '#/definitions/user')
end

context 'when deep ref' do
let(:instance) do
described_class.new(
type: :array,
items: { 'ref' => 'SomeModule::User' }
)
end

it 'modified @definition' do
expect(subject).to(
eq(
'type' => :array,
'items' => {
'$ref' => '#/definitions/user'
}
)
)
end
end
end
end
47 changes: 47 additions & 0 deletions spec/lib/bright_serializer/serializer_entity_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

RSpec.describe BrightSerializer::Serializer do
describe '.entity' do
subject { serializer_class.entity }

let(:serializer_class) do
Class.new do
include BrightSerializer::Serializer
attribute :name, entity: { type: :string }
end
end

it 'return name with his entity' do
expect(subject).to eq(name: { 'type' => :string })
end

context 'when one is undefined' do
let(:serializer_class) do
Class.new do
include BrightSerializer::Serializer
attribute :name, entity: { type: :string }
attribute :id
end
end

it 'return name with his entity' do
expect(subject).to eq(id: { type: :undefined }, name: { 'type' => :string })
end
end
end

describe '.entity_name' do
subject { serializer_class.entity_name }

let(:serializer_class) do
Class.new do
include BrightSerializer::Serializer
end
end

it 'return the class name downcase' do
allow(serializer_class).to receive(:name).and_return('BrightSerializer::Serializer::User')
expect(subject).to eq('user')
end
end
end

0 comments on commit c264c16

Please sign in to comment.