propel_facets 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/LICENSE +21 -0
- data/README.md +354 -0
- data/lib/generators/propel_facets/README.md +130 -0
- data/lib/generators/propel_facets/USAGE +68 -0
- data/lib/generators/propel_facets/install_generator.rb +152 -0
- data/lib/generators/propel_facets/templates/config/propel_facets.rb +188 -0
- data/lib/generators/propel_facets/templates/controllers/concerns/facet_renderer.rb +81 -0
- data/lib/generators/propel_facets/templates/controllers/concerns/strong_params_helper.rb +61 -0
- data/lib/generators/propel_facets/templates/doc/json_facet.md +78 -0
- data/lib/generators/propel_facets/templates/errors/application_error.rb +10 -0
- data/lib/generators/propel_facets/templates/errors/missing_facet.rb +13 -0
- data/lib/generators/propel_facets/templates/lib/api_params.rb +44 -0
- data/lib/generators/propel_facets/templates/models/application_record.rb +13 -0
- data/lib/generators/propel_facets/templates/models/concerns/model_facet.rb +189 -0
- data/lib/generators/propel_facets/test/propel_facets_generator_test.rb +75 -0
- data/lib/generators/propel_facets/test/test_helper.rb +20 -0
- data/lib/generators/propel_facets/unpack_generator.rb +307 -0
- data/lib/propel_facets.rb +3 -0
- metadata +86 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'unpack_generator'
|
3
|
+
##
|
4
|
+
# Generator to install propel_facets functionality in a Rails application
|
5
|
+
#
|
6
|
+
# This creates a COMPLETELY STANDALONE facets system with no gem dependencies.
|
7
|
+
# Usage:
|
8
|
+
# rails generate propel_facets:install # Full standalone system (runtime + generators)
|
9
|
+
#
|
10
|
+
# This generator:
|
11
|
+
# 1. Installs all runtime code (concerns, utilities, errors, config, etc.)
|
12
|
+
# 2. Automatically extracts generator logic to lib/generators/propel_facets/
|
13
|
+
# 3. Creates a system that requires NO gem dependencies
|
14
|
+
#
|
15
|
+
# After installation, you can remove 'propel_facets' from your Gemfile completely.
|
16
|
+
# All code is in your application and fully customizable.
|
17
|
+
#
|
18
|
+
module PropelFacets
|
19
|
+
class InstallGenerator < Rails::Generators::Base
|
20
|
+
source_root File.expand_path("templates", __dir__)
|
21
|
+
|
22
|
+
desc "Install PropelFacets functionality for Rails applications"
|
23
|
+
|
24
|
+
def copy_configuration_initializer
|
25
|
+
copy_file "config/propel_facets.rb", "config/initializers/propel_facets.rb"
|
26
|
+
end
|
27
|
+
|
28
|
+
def copy_model_concerns
|
29
|
+
copy_file "models/concerns/model_facet.rb", "app/models/concerns/model_facet.rb"
|
30
|
+
end
|
31
|
+
|
32
|
+
def copy_controller_concerns
|
33
|
+
copy_file "controllers/concerns/facet_renderer.rb", "app/controllers/concerns/facet_renderer.rb"
|
34
|
+
copy_file "controllers/concerns/strong_params_helper.rb", "app/controllers/concerns/strong_params_helper.rb"
|
35
|
+
end
|
36
|
+
|
37
|
+
def copy_utility_classes
|
38
|
+
copy_file "lib/api_params.rb", "lib/api_params.rb"
|
39
|
+
end
|
40
|
+
|
41
|
+
def copy_error_classes
|
42
|
+
copy_file "errors/application_error.rb", "app/errors/application_error.rb"
|
43
|
+
copy_file "errors/missing_facet.rb", "app/errors/missing_facet.rb"
|
44
|
+
end
|
45
|
+
|
46
|
+
def copy_documentation
|
47
|
+
copy_file "doc/json_facet.md", "doc/json_facet.md"
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_application_record_if_needed
|
51
|
+
application_record_path = "app/models/application_record.rb"
|
52
|
+
|
53
|
+
unless File.exist?(File.join(destination_root, application_record_path))
|
54
|
+
copy_file "models/application_record.rb", application_record_path
|
55
|
+
else
|
56
|
+
say_status :exists, application_record_path, :blue
|
57
|
+
add_propel_facets_to_application_record
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_generator_for_customization
|
62
|
+
generator_path = "lib/generators/propel_facets"
|
63
|
+
|
64
|
+
if File.exist?(generator_path)
|
65
|
+
say ""
|
66
|
+
say "š¦ Generator logic already extracted at #{generator_path}/", :blue
|
67
|
+
say "š” Skipping extraction to preserve your customizations", :cyan
|
68
|
+
else
|
69
|
+
say ""
|
70
|
+
say "š¦ Extracting generator logic for full customization...", :blue
|
71
|
+
|
72
|
+
# Automatically run the unpack generator to extract generator logic
|
73
|
+
invoke PropelFacets::UnpackGenerator, [], { force: false }
|
74
|
+
|
75
|
+
say ""
|
76
|
+
say "ā
Generator logic extracted to lib/generators/propel_facets/", :green
|
77
|
+
say "š” Your application is now completely standalone - no gem dependency needed for generators!", :cyan
|
78
|
+
say "šļø You can now remove 'propel_facets' from your Gemfile", :yellow
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def add_propel_facets_to_application_record
|
85
|
+
inject_into_file "app/models/application_record.rb", after: "class ApplicationRecord < ActiveRecord::Base\n" do
|
86
|
+
<<-RUBY
|
87
|
+
include ModelFacet
|
88
|
+
|
89
|
+
# Default JSON facets for all models
|
90
|
+
json_facet :reference, fields: [:id, :type]
|
91
|
+
json_facet :included, base: :reference
|
92
|
+
json_facet :short, base: :included, include_as: :reference
|
93
|
+
json_facet :details, base: :short, include_as: :included
|
94
|
+
|
95
|
+
RUBY
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def show_usage_instructions
|
100
|
+
say ""
|
101
|
+
say "PropelFacets has been installed!", :green
|
102
|
+
|
103
|
+
say "\nš¦ System Status: Completely standalone - no gem dependency!", :green
|
104
|
+
say "\nNext steps:", :yellow
|
105
|
+
say "⢠Optional: Remove 'propel_facets' from Gemfile (system is fully extracted)", :cyan
|
106
|
+
|
107
|
+
say ""
|
108
|
+
say "š§ Configuration:", :bold
|
109
|
+
say " Edit config/initializers/propel_facets.rb to customize:"
|
110
|
+
say " ⢠JSON root structure (:data, :model, :class, :none)"
|
111
|
+
say " ⢠API format (:rest, :jsonapi, :openapi, :graphql, etc.)"
|
112
|
+
say " ⢠Default facets and inheritance patterns"
|
113
|
+
say " ⢠Performance and caching options"
|
114
|
+
say ""
|
115
|
+
say "Usage in Models:", :bold
|
116
|
+
say " class User < ApplicationRecord"
|
117
|
+
say " json_facet :short, fields: [:name, :email]"
|
118
|
+
say " json_facet :details, base: :short, fields: [:created_at], methods: [:full_name]"
|
119
|
+
say " end"
|
120
|
+
say ""
|
121
|
+
say "Usage in Controllers:", :bold
|
122
|
+
say " class UsersController < ApplicationController"
|
123
|
+
say " include FacetRenderer"
|
124
|
+
say " include StrongParamsHelper"
|
125
|
+
say ""
|
126
|
+
say " connect_facet :short, actions: [:index]"
|
127
|
+
say " connect_facet :details, actions: [:show]"
|
128
|
+
say ""
|
129
|
+
say " permitted_params :name, :email, :role"
|
130
|
+
say ""
|
131
|
+
say " def index"
|
132
|
+
say " users = User.all"
|
133
|
+
say " render json: { data: users.map { |u| resource_json(u) } }"
|
134
|
+
say " end"
|
135
|
+
say " end"
|
136
|
+
say ""
|
137
|
+
say "Advanced Features:", :bold
|
138
|
+
say " permitted_unknown_params :metadata, :settings # Handle dynamic JSON"
|
139
|
+
say " PropelFacets.configure { |c| c.root = :none } # Flat JSON structure"
|
140
|
+
|
141
|
+
say ""
|
142
|
+
say "šØ Customization:", :bold
|
143
|
+
say "⢠Generator logic: lib/generators/propel_facets/install_generator.rb", :blue
|
144
|
+
say "⢠Templates: lib/generators/propel_facets/templates/", :blue
|
145
|
+
say "⢠Modify any part of the system - it's all yours now!", :cyan
|
146
|
+
|
147
|
+
say ""
|
148
|
+
say "š See doc/json_facet.md for complete documentation.", :blue
|
149
|
+
say "āļø See config/initializers/propel_facets.rb for all options.", :blue
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# PropelFacets Configuration
|
4
|
+
# This file was generated by: rails generate propel_facets:install
|
5
|
+
#
|
6
|
+
# PropelFacets provides a flexible system for defining different JSON
|
7
|
+
# representations of models and connecting them to controller actions.
|
8
|
+
|
9
|
+
require "rails"
|
10
|
+
|
11
|
+
module PropelFacets
|
12
|
+
# This module serves as the namespace for the Propel Facets gem.
|
13
|
+
# PropelFacets provides a flexible system for defining different JSON
|
14
|
+
# representations of models and connecting them to controller actions.
|
15
|
+
|
16
|
+
class Error < StandardError; end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield(configuration)
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset_configuration!
|
28
|
+
@configuration = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Configuration
|
33
|
+
# Currently supported options
|
34
|
+
attr_accessor :default_facets,
|
35
|
+
:strict_mode,
|
36
|
+
:default_base_facet,
|
37
|
+
:root,
|
38
|
+
:api_format
|
39
|
+
|
40
|
+
# Future features - not yet implemented
|
41
|
+
# attr_accessor :auto_include_concerns,
|
42
|
+
# :cache_facets,
|
43
|
+
# :naming_convention,
|
44
|
+
# :include_metadata,
|
45
|
+
# :date_format,
|
46
|
+
# :null_handling,
|
47
|
+
# :nested_depth_limit,
|
48
|
+
# :default_excluded_fields,
|
49
|
+
# :include_type_information,
|
50
|
+
# :pagination_format,
|
51
|
+
# :error_format,
|
52
|
+
# :performance_logging
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
# Default facets to create on ApplicationRecord
|
56
|
+
@default_facets = %w[reference included short details]
|
57
|
+
|
58
|
+
# Strict mode - raise errors for missing facets instead of warnings
|
59
|
+
@strict_mode = false
|
60
|
+
|
61
|
+
# Default base facet for inheritance
|
62
|
+
@default_base_facet = :reference
|
63
|
+
|
64
|
+
# JSON root structure options: :model, :class, :data, :none
|
65
|
+
# :model - { "user": {...} }, :class - { "User": {...} }
|
66
|
+
# :data - { "data": {...} }, :none - {...} (flat structure)
|
67
|
+
@root = :data
|
68
|
+
|
69
|
+
# API format standard: :openapi, :rest, :jsonapi, :graphql, :hal, :collection_json, :siren
|
70
|
+
@api_format = :rest
|
71
|
+
|
72
|
+
# Future configuration options (commented out until implemented):
|
73
|
+
|
74
|
+
# # Field naming convention: :snake_case, :camelCase, :PascalCase, :kebab_case
|
75
|
+
# @naming_convention = :snake_case
|
76
|
+
#
|
77
|
+
# # Include metadata (pagination, counts, etc.) in responses
|
78
|
+
# @include_metadata = true
|
79
|
+
#
|
80
|
+
# # Date/time format: :iso8601, :unix, :rfc3339, or custom strftime format
|
81
|
+
# @date_format = :iso8601
|
82
|
+
#
|
83
|
+
# # Null value handling: :include, :exclude, :empty_string
|
84
|
+
# @null_handling = :include
|
85
|
+
#
|
86
|
+
# # Maximum nesting depth to prevent infinite recursion
|
87
|
+
# @nested_depth_limit = 5
|
88
|
+
#
|
89
|
+
# # Fields to exclude by default from all facets (security sensitive)
|
90
|
+
# @default_excluded_fields = %w[password_digest encrypted_password reset_password_token confirmation_token]
|
91
|
+
#
|
92
|
+
# # Include model type information in JSON output
|
93
|
+
# @include_type_information = false
|
94
|
+
#
|
95
|
+
# # Pagination format: :cursor, :offset, :page, :limit_offset
|
96
|
+
# @pagination_format = :page
|
97
|
+
#
|
98
|
+
# # Error response format: :rails, :jsonapi, :rfc7807, :simple
|
99
|
+
# @error_format = :rails
|
100
|
+
#
|
101
|
+
# # Enable performance logging for facet rendering
|
102
|
+
# @performance_logging = Rails.env.development?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Configure PropelFacets with recommended defaults
|
108
|
+
PropelFacets.configure do |config|
|
109
|
+
# ==========================================
|
110
|
+
# CURRENTLY SUPPORTED CONFIGURATION OPTIONS
|
111
|
+
# ==========================================
|
112
|
+
|
113
|
+
# JSON root structure: :model, :class, :data, :none
|
114
|
+
# Examples:
|
115
|
+
# :data => { "data": { "id": 1, "name": "John" } }
|
116
|
+
# :model => { "user": { "id": 1, "name": "John" } }
|
117
|
+
# :none => { "id": 1, "name": "John" }
|
118
|
+
config.root = :data
|
119
|
+
|
120
|
+
# API format standard affects response structure and conventions
|
121
|
+
# :rest - Standard REST API responses
|
122
|
+
# :jsonapi - JSON:API specification (jsonapi.org)
|
123
|
+
# :openapi - OpenAPI/Swagger compatible
|
124
|
+
# :graphql - GraphQL-like nested structure
|
125
|
+
# :hal - Hypertext Application Language
|
126
|
+
# :collection_json - Collection+JSON hypermedia type
|
127
|
+
# :siren - Siren hypermedia specification
|
128
|
+
config.api_format = :rest
|
129
|
+
|
130
|
+
# Default facets available on all models
|
131
|
+
config.default_facets = %w[reference included short details]
|
132
|
+
|
133
|
+
# Error handling: raise errors (true) or show warnings (false) for missing facets
|
134
|
+
config.strict_mode = false
|
135
|
+
|
136
|
+
# Default base facet for inheritance chains
|
137
|
+
config.default_base_facet = :reference
|
138
|
+
|
139
|
+
# ==========================================
|
140
|
+
# FUTURE FEATURES - ROADMAP
|
141
|
+
# ==========================================
|
142
|
+
# Uncomment and configure as features are implemented
|
143
|
+
|
144
|
+
# # Automatically include ModelFacet in ApplicationRecord (currently manual via generator)
|
145
|
+
# config.auto_include_concerns = true
|
146
|
+
#
|
147
|
+
# # Cache compiled facets for performance (requires implementation)
|
148
|
+
# config.cache_facets = Rails.env.production?
|
149
|
+
#
|
150
|
+
# # Field naming convention for JSON output
|
151
|
+
# # :snake_case - user_name, created_at
|
152
|
+
# # :camelCase - userName, createdAt
|
153
|
+
# # :PascalCase - UserName, CreatedAt
|
154
|
+
# # :kebab_case - user-name, created-at
|
155
|
+
# config.naming_convention = :snake_case
|
156
|
+
#
|
157
|
+
# # Include metadata in responses (pagination info, counts, etc.)
|
158
|
+
# config.include_metadata = true
|
159
|
+
#
|
160
|
+
# # Date/time serialization format
|
161
|
+
# # :iso8601 - "2023-12-01T10:30:00Z"
|
162
|
+
# # :unix - 1701425400
|
163
|
+
# # :rfc3339 - "2023-12-01T10:30:00.000Z"
|
164
|
+
# # Custom: - "%Y-%m-%d %H:%M:%S"
|
165
|
+
# config.date_format = :iso8601
|
166
|
+
#
|
167
|
+
# # How to handle null/nil values in JSON
|
168
|
+
# # :include - Include null fields: { "name": null }
|
169
|
+
# # :exclude - Remove null fields: { }
|
170
|
+
# # :empty_string - Convert to empty: { "name": "" }
|
171
|
+
# config.null_handling = :include
|
172
|
+
#
|
173
|
+
# # Security: Fields to exclude from all facets by default
|
174
|
+
# config.default_excluded_fields = %w[
|
175
|
+
# password_digest encrypted_password password_hash
|
176
|
+
# reset_password_token confirmation_token unlock_token
|
177
|
+
# otp_secret_key encrypted_otp_secret
|
178
|
+
# ]
|
179
|
+
#
|
180
|
+
# # Performance and safety limits
|
181
|
+
# config.nested_depth_limit = 5
|
182
|
+
# config.performance_logging = Rails.env.development?
|
183
|
+
#
|
184
|
+
# # API response formats
|
185
|
+
# config.pagination_format = :page # :cursor, :offset, :page, :limit_offset
|
186
|
+
# config.error_format = :rails # :rails, :jsonapi, :rfc7807, :simple
|
187
|
+
# config.include_type_information = false # Include model class name in JSON
|
188
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Provide means to connect a controller to a JSON facetable model.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
#
|
8
|
+
# class MyController < ApiController
|
9
|
+
# connect_facet :short, actions: [:index]
|
10
|
+
# connect_facet :full, actions: [:show, :update, :create]
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
module FacetRenderer
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
attr_accessor :connected_facets
|
18
|
+
|
19
|
+
def inherited(subclass)
|
20
|
+
super(subclass)
|
21
|
+
subclass.connected_facets = @connected_facets.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# Directive to connect a facet
|
25
|
+
def connect_facet(facet, actions: nil)
|
26
|
+
@connected_facets = {} if @connected_facets.nil?
|
27
|
+
actual_actions = if actions.nil?
|
28
|
+
[:index, :create, :show, :update]
|
29
|
+
else
|
30
|
+
[actions].flatten
|
31
|
+
end
|
32
|
+
actual_actions.each do |this_action|
|
33
|
+
@connected_facets[this_action] = facet
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Gets facet for action name or nil
|
38
|
+
def action_facet(action)
|
39
|
+
@connected_facets = {} if @connected_facets.nil?
|
40
|
+
@connected_facets[action]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# To be used by controller to return params with visible and included fields
|
45
|
+
def resource_json(resource)
|
46
|
+
facet = self.class.action_facet(action_name.to_sym)
|
47
|
+
result = resource.as_json(
|
48
|
+
facet: [facet, :index, :default],
|
49
|
+
missing: :noaction
|
50
|
+
)
|
51
|
+
|
52
|
+
# get attachment url in single resource
|
53
|
+
if !resource.respond_to?('length') && resource.class.respond_to?('attachment_reflections')
|
54
|
+
set_attachments(result, resource)
|
55
|
+
else
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# set attachments to result json
|
61
|
+
def set_attachments(result, resource)
|
62
|
+
attachment_fields = {}
|
63
|
+
resource.class.attachment_reflections.keys.each do |name|
|
64
|
+
begin
|
65
|
+
attachment = resource.send(name.to_sym)
|
66
|
+
if attachment.is_a? ActiveStorage::Attached::Many
|
67
|
+
# Note: This is configured to return a maximum of 10 files.
|
68
|
+
# To retrieve all files within a specific parent model, utilize the 'Attachment' class.
|
69
|
+
attachment_fields[name.to_sym] = attachment.take(10).map {|a| url_for(a) if attachment.attached?}.compact
|
70
|
+
else
|
71
|
+
attachment_fields[name.to_sym] = url_for(attachment) if attachment.attached?
|
72
|
+
end
|
73
|
+
rescue StandardError => e
|
74
|
+
Rails.logger.error "Failed to generate URL for attachment #{name}: #{e.message}"
|
75
|
+
next
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
result&.merge(attachment_fields)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
# Helper for defining strong parameters in this application
|
6
|
+
#
|
7
|
+
# By including this into your controller, you will be allowed to define
|
8
|
+
# allowed parameters by calling `permitted_params` and passing a list
|
9
|
+
# of field names. This works specially for our application controllers
|
10
|
+
# structure where fields are found within `data` JSON property at the
|
11
|
+
# very root of a submitted payload
|
12
|
+
#
|
13
|
+
module StrongParamsHelper
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
attr_accessor :all_permitted_params, :all_permitted_unknown_params
|
18
|
+
|
19
|
+
# Directive to define permitted params list
|
20
|
+
def permitted_params(*names)
|
21
|
+
names.each do |name|
|
22
|
+
if @all_permitted_params.nil?
|
23
|
+
@all_permitted_params = [name]
|
24
|
+
elsif @all_permitted_params.include? name
|
25
|
+
warn "[ALREADY PERMITTED] `#{name}` was already permitted"
|
26
|
+
else
|
27
|
+
@all_permitted_params.push name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def permitted_unknown_params(*names)
|
33
|
+
names.each do |name|
|
34
|
+
if @all_permitted_unknown_params.nil?
|
35
|
+
@all_permitted_unknown_params = [name]
|
36
|
+
elsif @all_permitted_unknown_params.include? name
|
37
|
+
warn "[ALREADY PERMITTED] `#{name}` was already permitted"
|
38
|
+
else
|
39
|
+
@all_permitted_unknown_params.push name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# To be used by controller to parse params as strong params
|
46
|
+
def resource_params
|
47
|
+
permitted = self.class.all_permitted_params
|
48
|
+
|
49
|
+
api_params = ApiParams.new(params, request)
|
50
|
+
permitted_params = api_params.permit(permitted)
|
51
|
+
|
52
|
+
if self.class.all_permitted_unknown_params.present?
|
53
|
+
self.class.all_permitted_unknown_params.each do |key|
|
54
|
+
next unless (params.include?(:data) && !params[:data][key].nil?) || !params[key].nil?
|
55
|
+
permitted_params = permitted_params.merge({ key => api_params.force_permit(key) })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
permitted_params
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# JSON Facet System
|
2
|
+
|
3
|
+
JSON Facets provide a flexible way to define different JSON representations of your ActiveRecord models and automatically connect them to controller actions.
|
4
|
+
|
5
|
+
## Model Usage
|
6
|
+
|
7
|
+
Define facets in your models by including the `ModelFacet` concern:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class User < ApplicationRecord
|
11
|
+
# Basic facet with specific fields
|
12
|
+
json_facet :short, fields: [:name, :email, :created_at]
|
13
|
+
|
14
|
+
# Extended facet building on another
|
15
|
+
json_facet :details, base: :short,
|
16
|
+
fields: [:updated_at],
|
17
|
+
methods: [:full_name],
|
18
|
+
include: [:posts]
|
19
|
+
|
20
|
+
# With associations
|
21
|
+
json_facet :with_posts, base: :short, include: [:posts]
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
## Controller Usage
|
26
|
+
|
27
|
+
Connect facets to controller actions:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class UsersController < ApplicationController
|
31
|
+
include FacetRenderer
|
32
|
+
include StrongParamsHelper
|
33
|
+
|
34
|
+
connect_facet :short, actions: [:index]
|
35
|
+
connect_facet :details, actions: [:show, :update]
|
36
|
+
|
37
|
+
permitted_params :name, :email, :role
|
38
|
+
|
39
|
+
def index
|
40
|
+
users = User.all
|
41
|
+
render json: { data: users.map { |user| resource_json(user) } }
|
42
|
+
end
|
43
|
+
|
44
|
+
def show
|
45
|
+
user = User.find(params[:id])
|
46
|
+
render json: { data: resource_json(user) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## Parameter Handling
|
52
|
+
|
53
|
+
For complex JSON parameters:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class ProductsController < ApplicationController
|
57
|
+
include StrongParamsHelper
|
58
|
+
|
59
|
+
permitted_params :name, :price, :category
|
60
|
+
permitted_unknown_params :metadata, :settings # For dynamic JSON structures
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## Facet Options
|
65
|
+
|
66
|
+
- `fields`: Array of model attributes to include
|
67
|
+
- `methods`: Array of model methods to call and include
|
68
|
+
- `include`: Array of associations to include
|
69
|
+
- `include_as`: How to render included associations (uses their facets)
|
70
|
+
- `base`: Extend another facet definition
|
71
|
+
|
72
|
+
## Default Facets
|
73
|
+
|
74
|
+
All models inherit these default facets from `ApplicationRecord`:
|
75
|
+
- `:reference` - Basic id and type
|
76
|
+
- `:included` - Extends reference
|
77
|
+
- `:short` - General purpose summary view
|
78
|
+
- `:details` - Comprehensive view with associations
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Error thrown when a facet cannot be found
|
5
|
+
#
|
6
|
+
# This is only thrown when you pass `{ missing: :error }` as part of
|
7
|
+
# `as_json` options.
|
8
|
+
#
|
9
|
+
class MissingFacet < ApplicationError
|
10
|
+
def initialize(msg = 'Facet not found to render JSON')
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Helps manipulating client parameters for our API
|
5
|
+
#
|
6
|
+
class ApiParams
|
7
|
+
def initialize(params, request = nil)
|
8
|
+
@params = if params.is_a? ActionController::Parameters
|
9
|
+
params
|
10
|
+
else
|
11
|
+
ActionController::Parameters.new(params)
|
12
|
+
end
|
13
|
+
@user_agent = request&.user_agent
|
14
|
+
end
|
15
|
+
|
16
|
+
def permit(params)
|
17
|
+
if !@params.include?(:data) && command_line_agent
|
18
|
+
@params.permit params
|
19
|
+
else
|
20
|
+
@params.require(:data).permit(params)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def force_permit(key)
|
25
|
+
if !@params.include?(:data) && command_line_agent
|
26
|
+
@params[key].permit!
|
27
|
+
else
|
28
|
+
if @params[:data][key].is_a?(Array) # for nested attributes
|
29
|
+
@params[:data][key].each do |field|
|
30
|
+
field.permit!
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@params[:data][key].permit!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
CMDLINE_AGENTS = %w[httpie curl].freeze
|
39
|
+
|
40
|
+
def command_line_agent
|
41
|
+
agent = @user_agent&.downcase
|
42
|
+
CMDLINE_AGENTS.any? ->(prefix) { agent&.start_with? prefix }
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ApplicationRecord < ActiveRecord::Base
|
4
|
+
include ModelFacet
|
5
|
+
|
6
|
+
primary_abstract_class
|
7
|
+
|
8
|
+
# Default JSON facets for all models
|
9
|
+
json_facet :reference, fields: [:id, :type]
|
10
|
+
json_facet :included, base: :reference
|
11
|
+
json_facet :short, base: :included, include_as: :reference
|
12
|
+
json_facet :details, base: :short, include_as: :included
|
13
|
+
end
|