Skip to content

Commit

Permalink
Renderer - wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jhollinger committed Oct 22, 2024
1 parent 25f7964 commit 8361699
Show file tree
Hide file tree
Showing 21 changed files with 743 additions and 6 deletions.
16 changes: 16 additions & 0 deletions lib/blueprinter/v2.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# frozen_string_literal: true

require 'blueprinter/v2/base'

module Blueprinter
module V2
module Extensions
autoload :ConditionalFields, 'blueprinter/v2/extensions/conditional_fields'
autoload :DefaultValues, 'blueprinter/v2/extensions/default_values'
autoload :ExcludeIfNil, 'blueprinter/v2/extensions/exclude_if_nil'

DEFAULT = [
ConditionalFields,
DefaultValues,
ExcludeIfNil
].freeze
end
end
end
1 change: 1 addition & 0 deletions lib/blueprinter/v2/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module V2
:legacy_view,
:from,
:value_proc,
:extractor,
:options,
keyword_init: true
)
Expand Down
22 changes: 19 additions & 3 deletions lib/blueprinter/v2/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

require 'blueprinter/v2/dsl'
require 'blueprinter/v2/reflection'
require 'blueprinter/v2/render'
require 'blueprinter/v2/serializer'
require 'blueprinter/v2/view_builder'

module Blueprinter
Expand All @@ -12,10 +14,12 @@ class Base
extend Reflection

class << self
# Options set on this Blueprint
# Custom options set on this Blueprint for extensions
attr_accessor :options
# Extensions set on this Blueprint
attr_accessor :extensions
# Extractor class to use by default
attr_accessor :extractor
# @api private The fully-qualified name, e.g. "MyBlueprint", or "MyBlueprint.foo.bar"
attr_accessor :blueprint_name
# @api private
Expand All @@ -28,6 +32,7 @@ class << self
self.partials = {}
self.used_partials = []
self.extensions = []
self.extractor = nil
self.options = {}
self.blueprint_name = name
self.eval_mutex = Mutex.new
Expand All @@ -40,6 +45,7 @@ def self.inherited(subclass)
subclass.partials = partials.dup
subclass.used_partials = []
subclass.extensions = extensions.dup
subclass.extractor = extractor
subclass.options = options.dup
subclass.blueprint_name = subclass.name || blueprint_name
subclass.eval_mutex = Mutex.new
Expand Down Expand Up @@ -77,6 +83,7 @@ def self.[](name)
children ? view[children] : view
end

# MyBlueprint.render(obj).to_json
def self.render(obj, options = {})
if array_like? obj
render_collection(obj, options)
Expand All @@ -85,12 +92,20 @@ def self.render(obj, options = {})
end
end

# MyBlueprint.render_object(obj).to_json
def self.render_object(obj, options = {})
# TODO call external renderer
Render.new(obj, options, serializer: serializer, collection: false)
end

# MyBlueprint.render_collection(objs).to_json
def self.render_collection(objs, options = {})
# TODO call external renderer
Render.new(obj, options, serializer: serializer, collection: true)
end

# @api private
def self.serializer
eval! unless @evaled
@serializer
end

# Apply partials and field exclusions
Expand All @@ -115,6 +130,7 @@ def self.run_eval!
end

excludes.each { |f| fields.delete f }
@serializer = Serializer.new(self)
@evaled = true
end

Expand Down
12 changes: 9 additions & 3 deletions lib/blueprinter/v2/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ def use(*names)
#
# @param name [Symbol] Name of the field
# @param from [Symbol] Optionally specify a different method to call to get the value for "name"
# @param extractor [Class] Extractor class to use for this field
# @yield [TODO] Generate the value from the block
# @return [Blueprinter::V2::Field]
#
def field(name, from: name, **options, &definition)
def field(name, from: name, extractor: nil, **options, &definition)
fields[name.to_sym] = Field.new(
name: name,
from: from,
value_proc: definition,
extractor: extractor,
options: options.dup
)
end
Expand All @@ -62,10 +64,11 @@ def field(name, from: name, **options, &definition)
# @param blueprint [Class|Proc] Blueprint class to use, or one defined with a Proc
# @param view [Symbol] Only for use with legacy (not V2) blueprints
# @param from [Symbol] Optionally specify a different method to call to get the value for "name"
# @param extractor [Class] Extractor class to use for this association
# @yield [TODO] Generate the value from the block
# @return [Blueprinter::V2::Association]
#
def object(name, blueprint, from: name, view: nil, **options, &definition)
def object(name, blueprint, from: name, view: nil, extractor: nil, **options, &definition)
raise ArgumentError, 'The :view argument may not be used with V2 Blueprints' if view && blueprint.is_a?(V2)

fields[name.to_sym] = Association.new(
Expand All @@ -75,6 +78,7 @@ def object(name, blueprint, from: name, view: nil, **options, &definition)
legacy_view: view,
from: from,
value_proc: definition,
extractor: extractor,
options: options.dup
)
end
Expand All @@ -86,10 +90,11 @@ def object(name, blueprint, from: name, view: nil, **options, &definition)
# @param blueprint [Class|Proc] Blueprint class to use, or one defined with a Proc
# @param view [Symbol] Only for use with legacy (not V2) blueprints
# @param from [Symbol] Optionally specify a different method to call to get the value for "name"
# @param extractor [Class] Extractor class to use for this association
# @yield [TODO] Generate the value from the block
# @return [Blueprinter::V2::Association]
#
def collection(name, blueprint, from: name, view: nil, **options, &definition)
def collection(name, blueprint, from: name, view: nil, extractor: nil, **options, &definition)
raise ArgumentError, 'The :view argument may not be used with V2 Blueprints' if view && blueprint.is_a?(V2)

fields[name.to_sym] = Association.new(
Expand All @@ -99,6 +104,7 @@ def collection(name, blueprint, from: name, view: nil, **options, &definition)
legacy_view: view,
from: from,
value_proc: definition,
extractor: extractor,
options: options.dup
)
end
Expand Down
85 changes: 85 additions & 0 deletions lib/blueprinter/v2/extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

module Blueprinter
module V2
class Extension
class << self
attr_accessor :formatters
end

def self.inherited(ext)
ext.formatters = {}
end

#
# Add a formatter for instances of the given class.
#
# Example:
# class MyExtension < Blueprinter::V2::Extension
# format(Time) { |context| context.value.iso8601 }
# format Date, :date_str
#
# def date_str(context)
# context.value.iso8601
# end
# end
#
# @param klass [Class] The class of objects to format
# @param formatter_method [Symbol] Name of a public instance method to call for formatting
# @yield Do formatting in the block instead
#
def self.format(klass, formatter_method = nil, &formatter_block)
formatters[klass] = formatter_method || formatter_block
end

# Returning true will exclude this field and value from the results
# @param _context [Blueprinter::V2::Serializer::Context]
# @return [Boolean]
def exclude_field?(_context)
false
end

# Returning true will exclude this association and value from the results
# @param _context [Blueprinter::V2::Serializer::Context]
# @return [Boolean]
def exclude_object?(_context)
false
end

# Returning true will exclude this association and value from the results
# @param _context [Blueprinter::V2::Serializer::Context]
# @return [Boolean]
def exclude_collection?(_context)
false
end

# @param context [Blueprinter::V2::Serializer::Context]
# @return [Object]
def field_value(context)
context.value
end

# @param context [Blueprinter::V2::Serializer::Context]
# @return [Object]
def object_value(context)
context.value
end

# @param context [Blueprinter::V2::Serializer::Context]
# @return [Object]
def collection_value(context)
context.value
end

# Modify or replace the object passed to render/render_object/render_collection
def input(_blueprint, object, _options)
object
end

# Modify or replace the result before final render (e.g. to JSON)
def output(_blueprint, result, _options)
result
end
end
end
end
44 changes: 44 additions & 0 deletions lib/blueprinter/v2/extensions/conditional_fields.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require 'blueprinter/v2/extension'

module Blueprinter
module V2
module Extensions
class ConditionalFields < ::Blueprinter::V2::Extension
# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_field?(ctx)
if (cond = ctx.options[:field_if] || ctx.field.options[:if] || ctx.blueprint.class.options[:field_if])
ctx.blueprint.instance_exec(ctx, &cond)
elsif (cond = ctx.options[:field_unless] || ctx.field.options[:unless] || ctx.blueprint.class.options[:field_unless])
!ctx.blueprint.instance_exec(ctx, &cond)
else
false
end
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_object?(ctx)
if (cond = ctx.options[:object_if] || ctx.field.options[:if] || ctx.blueprint.class.options[:object_if])
ctx.blueprint.instance_exec(ctx, &cond)
elsif (cond = ctx.options[:object_unless] || ctx.field.options[:unless] || ctx.blueprint.class.options[:object_unless])
!ctx.blueprint.instance_exec(ctx, &cond)
else
false
end
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_collection?(ctx)
if (cond = ctx.options[:collection_if] || ctx.field.options[:if] || ctx.blueprint.class.options[:collection_if])
ctx.blueprint.instance_exec(ctx, &cond)
elsif (cond = ctx.options[:collection_unless] || ctx.field.options[:unless] || ctx.blueprint.class.options[:collection_unless])
!ctx.blueprint.instance_exec(ctx, &cond)
else
false
end
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/blueprinter/v2/extensions/default_values.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require 'blueprinter/v2/extension'

module Blueprinter
module V2
module Extensions
class DefaultValues < ::Blueprinter::V2::Extension
# @param ctx [Blueprinter::V2::Serializer::Context]
def field_value(ctx)
default_if = ctx.options[:field_default_if] || ctx.field.options[:default_if] || ctx.blueprint.class.options[:field_default_if]
return ctx.value unless ctx.value.nil? || default_if&.call(ctx.value)

ctx.options[:field_default] || ctx.field.options[:default] || ctx.blueprint.class.options[:field_default]
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def object_value(ctx)
default_if = ctx.options[:object_default_if] || ctx.field.options[:default_if] || ctx.blueprint.class.options[:object_default_if]
return ctx.value unless ctx.value.nil? || default_if&.call(ctx.value)

ctx.options[:object_default] || ctx.field.options[:default] || ctx.blueprint.class.options[:object_default]
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def collection_value(ctx)
default_if = ctx.options[:collection_default_if] || ctx.field.options[:default_if] || ctx.blueprint.class.options[:collection_default_if]
return ctx.value unless ctx.value.nil? || default_if&.call(ctx.value)

ctx.options[:collection_default] || ctx.field.options[:default] || ctx.blueprint.class.options[:collection_default]
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/blueprinter/v2/extensions/exclude_if_nil.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require 'blueprinter/v2/extension'

module Blueprinter
module V2
module Extensions
class ExcludeIfNil < Extension
# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_field?(ctx)
ctx.value.nil? && (ctx.options[:exclude_if_nil] || ctx.field.options[:exclude_if_nil] || ctx.blueprint.class.options[:exclude_if_nil])
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_object?(...)
exclude_field?(...)
end

# @param ctx [Blueprinter::V2::Serializer::Context]
def exclude_collection?(...)
exclude_field?(...)
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/blueprinter/v2/extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Blueprinter
module V2
# The default extractor, and the suggested base class for custom extractors
class Extractor
def field(blueprint_instance, field, object, options)
if field.value_proc
blueprint_instance.instance_exec(object, options, &field.value_proc)
elsif object.is_a? Hash
object[field.from]
else
object.public_send(field.from)
end
end

def object(blueprint_instance, field, object, options)
field(blueprint_instance, field, object, options)
end

def collection(blueprint_instance, field, object, options)
field(blueprint_instance, field, object, options)
end
end
end
end
1 change: 1 addition & 0 deletions lib/blueprinter/v2/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module V2
:name,
:from,
:value_proc,
:extractor,
:options,
keyword_init: true
)
Expand Down
Loading

0 comments on commit 8361699

Please sign in to comment.