PropelFacets

A Rails generator that provides a flexible system for defining different JSON representations of ActiveRecord models and automatically connecting them to controller actions.

Installation

PropelFacets is designed as a self-extracting generator gem. You install it temporarily, run the generators to extract the code into your application, then remove the gem dependency.

Step 1: Add to Gemfile as a Path Gem

# In your Gemfile
gem 'propel_facets', path: 'propel_facets'

Step 2: Bundle Install

bundle install

Step 3: Unpack the Generator (Optional)

If you want to customize the generator templates:

rails generate propel_facets:unpack

This extracts the generator into lib/generators/propel_facets/ for customization.

Step 4: Install PropelFacets

rails generate propel_facets:install

This installs the facets system including model and controller concerns, utilities, and configuration.

Step 5: Remove Gem Dependency (Optional)

After installation, you can remove the gem from your Gemfile. All functionality remains in your application.

Quick Start

In Models

Define different JSON representations using facets:

class User < ApplicationRecord
  # Basic facet with specific fields
  json_facet :summary, fields: [:id, :name, :email, :created_at]

  # Extended facet building on another
  json_facet :details, base: :summary, 
             fields: [:updated_at], 
             methods: [:full_name], 
             include: [:posts]

  # Association facets
  json_facet :with_posts, base: :summary, include: [:posts]

  # Custom method
  def full_name
    "#{first_name} #{last_name}"
  end
end

In Controllers

Connect facets to controller actions:

class UsersController < ApplicationController
  include FacetRenderer
  include StrongParamsHelper

  connect_facet :summary, actions: [:index]
  connect_facet :details, actions: [:show, :update]

  permitted_params :name, :email, :role

  def index
    users = User.all
    render json: { data: users.map { |user| resource_json(user) } }
  end

  def show
    user = User.find(params[:id])
    render json: { data: resource_json(user) }
  end
end

Enhanced Parameter Handling

For complex JSON parameters:

class ProductsController < ApplicationController
  include StrongParamsHelper

  permitted_params :name, :price, :category
  permitted_unknown_params :metadata, :settings  # For dynamic JSON structures

  def create
    # Handles both strong params and unknown params
    product = Product.new(resource_params)
    # ...
  end
end

Features

Flexible JSON Representations

  • Multiple facets per model - Define various JSON views for different contexts
  • Facet inheritance - Build complex facets by extending simpler ones
  • Field selection - Include specific model attributes
  • Method inclusion - Include results of model methods
  • Association rendering - Include related models with their own facets

Controller Integration

  • Automatic facet-action mapping - Connect specific facets to controller actions
  • Resource JSON rendering - Simple method to render models with appropriate facets
  • Enhanced parameter handling - Support for complex JSON structures
  • Strong parameters extension - Handles both known and unknown parameters

Configuration System

  • JSON root structures - Configure response format (:data, :model, :class, :none)
  • API format standards - Support for REST, JSON:API, OpenAPI, GraphQL formats
  • Strict mode - Control error handling for missing facets
  • Default facets - Set up standard facets across all models

Rails Integration

  • ApplicationRecord defaults - Standard facets available on all models
  • ActiveStorage support - Automatic attachment URL handling
  • Standard Rails patterns - Follows Rails conventions throughout
  • Zero dependencies - No runtime gem dependencies after extraction

Facet Options

When defining facets, you can use these options:

  • fields: Array of model attributes to include
  • methods: Array of model methods to call and include
  • include: Array of associations to include (uses association name as JSON key)
  • include_as: Hash of associations with custom JSON key names
  • base: Name of another facet to extend from
class Article < ApplicationRecord
  # Basic reference facet
  json_facet :reference, fields: [:id, :title]

  # Summary extends reference
  json_facet :summary, base: :reference, fields: [:excerpt, :published_at]

  # Details with methods and associations
  json_facet :details, base: :summary,
             methods: [:word_count, :reading_time],
             include: [:author],           # Will appear as "author" in JSON
             include_as: [comments: :feedback]  # Will appear as "feedback" in JSON
end

Default Facets

All models inherit these default facets from ApplicationRecord:

  • :reference - Basic id and type information
  • :included - Extends reference facet
  • :short - General purpose summary view
  • :details - Comprehensive view with more fields
# These are automatically available on all models:
User.first.as_json(facet: :reference)  # => { "id": 1, "type": "User" }
User.first.as_json(facet: :short)      # => More fields based on inheritance

Configuration

Configure PropelFacets in config/initializers/propel_facets.rb:

PropelFacets.configure do |config|
  # JSON root structure: :data, :model, :class, :none
  # :data  => { "data": { "id": 1, "name": "John" } }
  # :model => { "user": { "id": 1, "name": "John" } }
  # :none  => { "id": 1, "name": "John" }
  config.root = :data

  # API format standard: :rest, :jsonapi, :openapi, :graphql
  config.api_format = :rest

  # Default facets available on all models
  config.default_facets = %w[reference included short details]

  # Error handling: raise errors (true) or show warnings (false)
  config.strict_mode = false

  # Default base facet for inheritance chains
  config.default_base_facet = :reference
end

Advanced Usage

Complex Facet Inheritance

class Product < ApplicationRecord
  # Base facets
  json_facet :reference, fields: [:id, :name]
  json_facet :pricing, fields: [:price, :currency]

  # Combine multiple facets
  json_facet :listing, base: :reference, 
             fields: [:description, :availability],
             methods: [:formatted_price]

  # Full details combining multiple concerns
  json_facet :admin, base: :listing,
             fields: [:cost, :margin, :created_at],
             include: [:supplier, :reviews]
end

Dynamic Parameter Handling

class ApiController < ApplicationController
  include StrongParamsHelper

  # Handle known parameters
  permitted_params :name, :email, :status

  # Handle dynamic JSON structures
  permitted_unknown_params :preferences, :metadata, :custom_fields

  private

  def resource_params
    # Returns both strong params and unknown params merged
    super
  end
end

Custom JSON Formatting

# Configure different JSON root structures
PropelFacets.configure do |config|
  config.root = :none  # Flat JSON structure
end

# Result:
User.first.as_json(facet: :summary)
# => { "id": 1, "name": "John", "email": "[email protected]" }

# vs. with config.root = :data:
# => { "data": { "id": 1, "name": "John", "email": "[email protected]" } }

Self-Extracting Architecture

PropelFacets follows a self-extracting pattern that provides:

  • No runtime dependencies - all code lives in your application
  • Full control - modify any component after installation
  • No black boxes - transparent, readable Rails code
  • Easy maintenance - standard Rails patterns throughout

After installation, you can:

  • Remove the gem from your Gemfile
  • Customize all generated code
  • Maintain and extend functionality independently
  • Modify facet behavior for your specific needs

Generated Files

After installation, PropelFacets creates:

Model Support

  • app/models/concerns/model_facet.rb - Facet definition DSL for models
  • app/models/application_record.rb - Base model with default facets (if needed)

Controller Support

  • app/controllers/concerns/facet_renderer.rb - Controller facet rendering
  • app/controllers/concerns/strong_params_helper.rb - Enhanced parameter handling

Utilities

  • lib/api_params.rb - API parameter utility class

Error Handling

  • app/errors/application_error.rb - Base error class
  • app/errors/missing_facet.rb - Facet-specific error handling

Configuration

  • config/initializers/propel_facets.rb - PropelFacets configuration

Documentation

  • doc/json_facet.md - Complete usage documentation and examples

Integration with PropelApi

PropelFacets integrates seamlessly with PropelApi for complete API development:

# Install both systems
rails generate propel_api:install --adapter=propel_facets
rails generate propel_facets:install

# Generated API controllers automatically use facets
class Api::V1::UsersController < Api::V1::ApiController
  connect_facet :short, actions: [:index]
  connect_facet :details, actions: [:show, :create, :update]

  # Facet rendering is automatic
end

Development

# Run facets tests
cd propel_facets
bundle exec rake test

# Test specific functionality
bundle exec ruby -Ilib:test test/propel_facets_generator_test.rb

Roadmap

Planned Features

  • Caching integration - Rails caching support for rendered facets
  • Field naming conventions - camelCase, kebab-case transformations
  • Metadata inclusion - Pagination, counts, timestamps
  • Performance logging - Monitor facet rendering performance
  • Nested depth limits - Prevent infinite recursion
  • Type information - Include model type data in JSON

Contributing

Bug reports and pull requests are welcome on GitHub.

License

The gem is available as open source under the MIT License.