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 includemethods: Array of model methods to call and includeinclude: Array of associations to include (uses association name as JSON key)include_as: Hash of associations with custom JSON key namesbase: 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 modelsapp/models/application_record.rb- Base model with default facets (if needed)
Controller Support
app/controllers/concerns/facet_renderer.rb- Controller facet renderingapp/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 classapp/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.