Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define entity in serializer for grape_swagger #9

Merged
merged 8 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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