New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

props_template

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

props_template

  • 0.37.0
  • Rubygems
  • Socket score

Version published
Maintainers
1
Created
Source

PropsTemplate

PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has support for Russian-Doll caching, layouts, and can be queried by giving the root a key path.

Build
Status

It's fast.

PropsTemplate bypasses the steps of hash building and serializing that other libraries perform by using Oj's StringWriter in rails mode.

benchmarks

Caching is fast too.

While other libraries spend time unmarshaling, merging hashes, and serializing to JSON; PropsTemplate simply takes the cached string and uses Oj's push_json.

Example:

PropsTemplate is very similar to JBuilder, and selectively retains some conveniences and magic.

json.flash flash.to_h

json.menu do
  json.currentUser do
    json.email current_user.email
    json.avatar current_user.avatar
    json.inbox current_user.messages.count
  end
end

json.dashboard(defer: :auto) do
  sleep 5
  json.complexPostMetric 500
end

json.posts do
  page_num = params[:page_num]
  paged_posts = @posts.page(page_num).per(20)

  json.list do
    json.array! paged_posts, key: :id do |post|
      json.id post.id
      json.description post.description
      json.commentsCount post.comments.count
      json.editPath edit_post_path(post)
    end
  end

  json.paginationPath posts_path
  json.current pagedPosts.current_page
  json.total @posts.count
end

json.footer partial: 'shared/footer' do
end

Installation

gem 'props_template'

and run bundle.

Optionally add the core ext to an initializer if you want to dig into your templates.

require 'props_template/core_ext'

And create a file in your app/views folder like so:

# app/views/posts/index.json.props

json.greetings "hello world"

You can also add a layout.

API

json.set! or json.<your key here>

Defines the attribute or structure. All keys are not formatted by default. See Change Key Format to change this behavior.

json.set! :authorDetails, {...options} do
  json.set! :firstName, 'David'
end

or

json.authorDetails, {...options} do
  json.firstName 'David'
end


# => {"authorDetails": { "firstName": "David" }}

The inline form defines key and value

ParameterNotes
keyA json object key
valueA value

json.set! :firstName, 'David'

or

json.firstName 'David'

# => { "firstName": "David" }

The block form defines key and structure

ParameterNotes
keyA json object key
optionsAdditional options
blockAdditional json.set!s or json.array!s
json.set! :details do
  ...
end

or

json.details do
  ...
end

The difference between the block form and inline form is

  1. The block form is an internal node. Functionality such as Partials, Deferment and other options are only available on the block form.
  2. The inline form is considered a leaf node, and you can only dig for internal nodes.

json.extract!

Extracts attributes from object or hash in 1 line

# without extract!
json.id user.id
json.email user.email
json.firstName user.first_name

# with extract!
json.extract! user, :id, :email, :first_name

# => {"id" => 1, "email" => "email@gmail.com", "first_name" => "user"}

# with extract! with key transformation
json.extract! user, :id, [:first_name, :firstName], [:last_name, :lastName]

# => {"id" => 1, "firstName" => "user", "lastName" => "last"}

The inline form defines object and attributes

ParameterNotes
objectAn object
attributesA list of attributes

json.array!

Generates an array of json objects.

collection = [ {name: 'john'}, {name: 'jim'} ]

json.details do
  json.array! collection, {...options} do |person|
    json.firstName person[:name]
  end
end

# => {"details": [{"firstName": 'john'}, {"firstName": 'jim'} ]}
ParameterNotes
collectionA collection that responds to member_at and member_by
optionsAdditional options

To support digging, any list passed to array! MUST implement member_at(index) and member_by(attr, value).

For example, if you were using a delegate:

class ObjectCollection < SimpleDelegator
  def member_at(index)
    at(index)
  end

  def member_by(attr, val)
    find do |ele|
      ele[attr] == val
    end
  end
end

Then in your template:

data = ObjectCollection.new([
  {id: 1, name: 'foo'},
  {id: 2, name: 'bar'}
])

json.array! data do
  ...
end

Similarly for ActiveRecord:

class ApplicationRecord < ActiveRecord::Base
  def self.member_at(index)
    offset(index).limit(1).first
  end

  def self.member_by(attr, value)
    find_by(Hash[attr, val])
  end
end

Then in your template:

json.array! Post.all do
  ...
end
Array core extension

For convenience, PropsTemplate includes a core_ext that adds these methods to Array. For example:

require 'props_template/core_ext'
data = [
  {id: 1, name: 'foo'},
  {id: 2, name: 'bar'}
]

json.posts
  json.array! data do
    ...
  end
end

PropsTemplate does not know what the elements are in your collection. The example above will be fine for digging by index, but will raise a NotImplementedError if you query by attribute. You may still need to implement member_by.

json.deferred!

Returns all deferred nodes used by the deferment option.

Note This is a SuperglueJS specific functionality and is used in application.json.props when first running rails superglue:install:web

json.deferred json.deferred!

# => [{url: '/some_url?props_at=outer.inner', path: 'outer.inner', type: 'auto'}]

This method provides metadata about deferred nodes to the frontend (SuperglueJS) to fetch missing data in a second round trip.

json.fragments!

Returns all fragment nodes used by the partial fragments option.

ruby json.fragments json.fragments!

Note This is a SuperglueJS specific functionality and is used in application.json.props when first running rails superglue:install:web

Options

Options Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.

json.post(partial: 'blog_post') do
end

Partials

Partials are supported. The following will render the file views/posts/_blog_posts.json.props, and set a local variable post assigned with @post, which you can use inside the partial.

json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
end

Usage with arrays:

# The `as:` option is supported when using `array!`
# Without `as:` option you can use blog_post variable (name is based on partial's name) inside partial

json.posts do
  json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
  end
end

Rendering partials without a key is also supported using json.partial!, but use sparingly! json.partial! is not optimized for collection rendering and may cause performance problems. Its best used for things like a shared header or footer.

Do:

json.partial! partial: "header", locals: {user: @user} do
end

or

json.posts do
  json.array! @posts, partial: ["posts/blog_post", locals: {post: @post}] do
  end
end

Do NOT:

@post.each do |post|
  json.partial! partial: "post", locals: {post: @post} do
  end
end

Partial Fragments

Note This is a SuperglueJS specific functionality.

A fragment identifies a partial output across multiple pages. It can be used to update cross cutting concerns like a header bar.

# index.json.props
json.header partial: ["profile", fragment: "header"] do
end

# _profile.json.props
json.profile do
  json.address do
    json.state "New York City"
  end
end

When using fragments with Arrays, the argument MUST be a lamda:

require 'props_template/core_ext'

json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}] do
end

Caching

Caching is supported on internal nodes only. This limitation is what makes it possible to for props_template to forgo marshalling/unmarshalling and simply use push_json.

Usage:

json.author(cache: "some_cache_key") do
  json.firstName "tommy"
end

#or

json.profile(cache: "cachekey", partial: ["profile", locals: {foo: 1}]) do
end

#or nest it

json.author(cache: "some_cache_key") do
  json.address(cache: "some_other_cache_key") do
    json.zip 11214
  end
end

When used with arrays, PropsTemplate will use Rails.cache.read_multi.

require 'props_template/core_ext'

opts = { cache: ->(i){ ['a', i] } }

json.array! [4,5], opts do |x|
  json.top "hello" + x.to_s
end

#or on arrays with partials

opts = { cache: (->(d){ ['a', d.id] }), partial: ["blog_post", as: :blog_post] }

json.array! @options, opts do
end

Deferment

You can defer rendering of expensive nodes in your content tree using the defer: :manual option. Behind the scenes PropsTemplates will no-op the block entirely and replace the value with a placeholder. A common use case would be tabbed content that does not load until you click the tab.

When your client receives the payload, you may issue a second request to the same endpoint to fetch any missing nodes. See digging

There is also an defer: :auto option that you can use with SuperglueJS. SuperglueJS will use the metadata from json.deferred! to issue a remote dispatch to fetch the missing node and immutably graft it at the appropriate keypath in your Redux store.

Usage:

json.dashboard(defer: :manual) do
  sleep 10
  json.someFancyMetric 42
end


# or you can explicitly pass a placeholder

json.dashboard(defer: [:manual, placeholder: {}]) do
  sleep 10
  json.someFancyMetric 42
end

A auto option is available:

Note This is a SuperglueJS specific functionality.

json.dashboard(defer: :auto) do
  sleep 10
  json.someFancyMetric 42
end

Finally in your application.json.props:

json.defers json.deferred!
Working with arrays

The default behavior for deferements is to use the index of the collection to identify an element.

Note If you are using this library with SuperglueJS, the :auto options will generate ?props_at=a.b.c.0.title for json.deferred!.

If you wish to use an attribute to identify the element. You must:

  1. Use the :key option on json.array!. This key refers to an attribute on your collection item, and is used for defer: :auto to generate a keypath for SuperglueJS. If you are NOT using SuperglueJS, you do not need to do this.

  2. Implement member_at, on the collection. This will be called by PropsTemplate to when digging

For example:

require 'props_template/core_ext'
data = [
  {id: 1, name: 'foo'},
  {id: 2, name: 'bar'}
]

json.posts
  json.array! data, key: :some_id do |item|
    # By using :key, props_template will append `json.some_id item.some_id`
    # automatically

    json.contact(defer: :auto) do
      json.address '123 example drive'
    end
  end
end

If you are using SuperglueJS, SuperglueJS will, it will automatically kick off remote(?props_at=posts.some_id=1.contact) and remote(?props_at=posts.some_id=2.contact).

Digging

PropsTemplate has the ability to walk the tree you build, skipping execution of untargeted nodes. This feature is useful for selectively updating your frontend state.

traversal_path = ['data', 'details', 'personal']

json.data(dig: traversal_path) do
  json.details do
    json.employment do
      ...more stuff
    end

    json.personal do
      json.name 'james'
      json.zipCode 91210
    end
  end
end

json.footer do
  ...
end

PropsTemplate will walk depth first, walking only when it finds a matching key, then executes the associated block, and repeats until it the node is found. The above will output:

{
  "data": {
    "name": 'james',
    "zipCode": 91210
  },
  "footer": {
    ...
  }
}

Digging only works with blocks, and will NOT work with Scalars ("leaf" values). For example:

traversal_path = ['data', 'details', 'personal', 'name'] <- not found

json.data(dig: traversal_path) do
  json.details do
    json.personal do
      json.name 'james'
    end
  end
end

Nodes that do not exist

Nodes that are not found will remove the branch where digging was enabled on.

traversal_path = ['data', 'details', 'does_not_exist']

json.data(dig: traversal_path) do
  json.details do
    json.personal do
      json.name 'james'
    end
  end
end

json.footer do
  ...
end

The above will render:

{
  "footer": {
    ...
  }
}

Layouts

A single layout is supported. To use, create an application.json.props in app/views/layouts. Here's an example:

json.data do
  # template runs here.
  yield json
end

json.header do
  json.greeting "Hello"
end

json.footer do
  json.greeting "Hello"
end

json.flash flash.to_h

NOTE PropsTemplate inverts the usual Rails rendering flow. PropsTemplate will render Layout first, then the template when yield json is used.

Change key format

By default, keys are not formatted. This is intentional. By being explicity with your keys, it makes your views quicker and more easily diggable when working in Javascript land.

If you must change this behavior, override it in an initializer and cache the value:

# default behavior
Props::BaseWithExtensions.class_eval do
  # json.firstValue "first"
  # json.second_value "second"
  #
  # -> { "firstValue" => "first", "second_value" => "second" }
  def key_format(key)
    key.to_s
  end
end

# camelCased behavior
Props::BaseWithExtensions.class_eval do
  # json.firstValue "first"
  # json.second_value "second"
  #
  # -> { "firstValue" => "first", "secondValue" => "second" }
  def key_format(key)
    @key_cache ||= {}
    @key_cache[key] ||= key.to_s.camelize(:lower)
    @key_cache[key]
  end

  def result!
    result = super
    @key_cache = {}
    result
  end
end

# snake_cased behavior
Props::BaseWithExtensions.class_eval do
  # json.firstValue "first"
  # json.second_value "second"
  #
  # -> { "first_value" => "first", "second_value" => "second" }
  def key_format(key)
    @key_cache ||= {}
    @key_cache[key] ||= key.to_s.underscore
    @key_cache[key]
  end

  def result!
    result = super
    @key_cache = {}
    result
  end
end

Escape mode

PropsTemplate runs OJ with mode: :rails, which escapes HTML and XML characters such as & and <.

Contributing

See the CONTRIBUTING document. Thank you, contributors!

Special Thanks

Thanks to turbostreamer, oj, and jbuilder for the inspiration.

FAQs

Package last updated on 31 Dec 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc