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

Feature: rendering optional attributes #31

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions lib/props_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class << self
self.template_lookup_options = {handlers: [:props]}

delegate :result!, :array!,
:optional!,
:deferred!,
:fragments!,
:set_block_content!,
Expand Down
33 changes: 33 additions & 0 deletions lib/props_template/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ class InvalidScopeForArrayError < StandardError; end
class InvalidScopeForObjError < StandardError; end

class Base
attr_accessor :contains

def initialize(encoder = nil)
@stream = Oj::StringWriter.new(mode: :rails)
@scope = nil
@contains = nil
end

def set_block_content!(options = {})
Expand Down Expand Up @@ -87,6 +90,21 @@ def handle_collection(collection, options)
end
end

# builds structure of rendered optional attributes as hash
def transform_contain_keys(values)
return {} if values.blank?

values.each_with_object({}) do |item, acc|
if item.is_a?(Hash)
item.each do |key, value|
acc[key] = transform_contain_keys(value)
end
else
acc[item] = :present
end
end
end

# todo, add ability to define contents of array
def array!(collection, options = {})
if @scope.nil?
Expand All @@ -105,6 +123,21 @@ def array!(collection, options = {})
nil
end

# value should be lambda to avoid calculations before optional check
# if attribute should be rendered then call set with calculated value
def optional!(key, value = nil, &block)
return set!(key, value ? value.call : {}, &block) if contains.blank?

# traveled path without arra indexes
path = traveled_path.select { |item| item.is_a?(Symbol) }
# render if
# 1) "contains" hash contains key at required nesting
# 2) "contains" hash contains parent key at required nesting with all rendering attributes
render_attribute = contains.dig(*(path + [key])) || path.size > 0 && contains.dig(*path) == {}

set!(key, value ? value.call : {}, &block) if render_attribute
end

def result!
if @scope.nil?
@stream.push_object
Expand Down
1 change: 1 addition & 0 deletions lib/props_template/base_with_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def initialize(builder, context = nil, options = {})
@builder = builder
@em = ExtensionManager.new(self)
@traveled_path = []

super()
end

Expand Down
5 changes: 5 additions & 0 deletions lib/props_template/extensions/partial_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ def refine_options(options, item = nil)
locals = (rest[:locals] || {}).clone
rest[:locals] = locals

if rest
@base.contains ||= @base.transform_contain_keys(rest.dig(:locals, :contains))
rest[:locals][:contains] = @base.contains
end

if item
as = if !rest[:as]
retrieve_variable(partial)
Expand Down
8 changes: 8 additions & 0 deletions lib/props_template/searcher.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
module Props
class Searcher
attr_accessor :contains
attr_reader :builder, :context, :fragments, :traveled_path

delegate :transform_contain_keys, to: :builder!

def initialize(builder, path = [], context = nil)
@search_path = path
@depth = 0
Expand All @@ -10,9 +13,14 @@ def initialize(builder, path = [], context = nil)
@found_options = nil
@builder = builder
@traveled_path = []
@contains = nil
@partialer = Partialer.new(self, context, builder)
end

def builder!
@builder
end

def deferred!
[]
end
Expand Down
66 changes: 66 additions & 0 deletions spec/extensions/partial_render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,70 @@

expect(json).to eql_json([])
end

context "when locals with contains" do
it "renders only required attributes" do
json = render(<<~PROPS)
emails = [
{value: '[email protected]'},
{value: '[email protected]'},
]
json.array! emails, partial: ['optional', locals: {contains: [:email]}] do
end
PROPS

expect(json).to eql_json([
{email: "[email protected]"},
{email: "[email protected]"}
])
end

it "renders object with all nested attributes" do
json = render(<<~PROPS)
emails = [
{value: '[email protected]'},
{value: '[email protected]'},
]
json.array! emails, partial: ['optional', locals: {contains: [emailAsObject: []]}] do
end
PROPS

expect(json).to eql_json([
{emailAsObject: {email: "[email protected]", something: nil}},
{emailAsObject: {email: "[email protected]", something: nil}}
])
end

it "renders object with only required nested attributes" do
json = render(<<~PROPS)
emails = [
{value: '[email protected]'},
{value: '[email protected]'},
]
json.array! emails, partial: ['optional', locals: {contains: [emailAsObject: [:email]]}] do
end
PROPS

expect(json).to eql_json([
{emailAsObject: {email: "[email protected]"}},
{emailAsObject: {email: "[email protected]"}}
])
end

it "renders all attributes" do
json = render(<<~PROPS)
emails = [
{value: '[email protected]'},
{value: '[email protected]'},
]
json.array! emails, partial: ['optional', locals: {contains: [:email, {emailAsObject: [:email]}, :something]}] do
end
PROPS

expect(json).to eql_json([
{email: "[email protected]", something: nil, emailAsObject: {email: "[email protected]"}},
{email: "[email protected]", something: nil, emailAsObject: {email: "[email protected]"}}
])
end
end
end
7 changes: 7 additions & 0 deletions spec/fixtures/_optional.json.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
json.optional! :email, -> { optional[:value] }
json.optional! :something, -> { nil }

json.optional! :emailAsObject do
json.optional! :email, -> { optional[:value] }
json.optional! :something, -> { nil }
end
48 changes: 48 additions & 0 deletions spec/props_template_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,52 @@
}.to raise_error(Props::InvalidScopeForArrayError)
end
end

context "optional!" do
it "sets a value" do
json = Props::Base.new
json.optional! :foo, -> { "bar" }
attrs = json.result!.strip

expect(attrs).to eql_json({
foo: "bar"
})
end

it "sets a empty obj when block is empty" do
json = Props::Base.new
json.optional! :foo do
end
attrs = json.result!.strip

expect(attrs).to eql_json({
foo: {}
})
end

it "sets a empty obj when nested block is empty" do
json = Props::Base.new
json.optional! :foo do
json.optional! :bar do
end
end
attrs = json.result!.strip

expect(attrs).to eql_json({
foo: {
bar: {}
}
})
end

it "sets a null value" do
json = Props::Base.new
json.optional! :foo, -> { nil }
attrs = json.result!.strip

expect(attrs).to eql_json({
foo: nil
})
end
end
end
Loading