jpie 0.3.0
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/.aiconfig +65 -0
- data/.rubocop.yml +140 -0
- data/CHANGELOG.md +93 -0
- data/LICENSE.txt +21 -0
- data/README.md +1032 -0
- data/Rakefile +19 -0
- data/jpie.gemspec +48 -0
- data/lib/jpie/configuration.rb +12 -0
- data/lib/jpie/controller/crud_actions.rb +110 -0
- data/lib/jpie/controller/error_handling.rb +41 -0
- data/lib/jpie/controller/parameter_parsing.rb +35 -0
- data/lib/jpie/controller/rendering.rb +60 -0
- data/lib/jpie/controller.rb +18 -0
- data/lib/jpie/deserializer.rb +110 -0
- data/lib/jpie/errors.rb +70 -0
- data/lib/jpie/generators/resource_generator.rb +39 -0
- data/lib/jpie/generators/templates/resource.rb.erb +12 -0
- data/lib/jpie/railtie.rb +36 -0
- data/lib/jpie/resource/attributable.rb +98 -0
- data/lib/jpie/resource/inferrable.rb +43 -0
- data/lib/jpie/resource/sortable.rb +93 -0
- data/lib/jpie/resource.rb +107 -0
- data/lib/jpie/serializer.rb +205 -0
- data/lib/jpie/version.rb +5 -0
- data/lib/jpie.rb +26 -0
- metadata +223 -0
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
RuboCop::RakeTask.new
|
9
|
+
|
10
|
+
desc 'Run Brakeman security scanner'
|
11
|
+
task brakeman: :environment do
|
12
|
+
require 'brakeman'
|
13
|
+
Brakeman.run app_path: '.', print_report: true, exit_on_warn: true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Run all checks (RSpec, RuboCop, Brakeman)'
|
17
|
+
task check: %i[spec rubocop brakeman]
|
18
|
+
|
19
|
+
task default: :check
|
data/jpie.gemspec
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/jpie/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'jpie'
|
7
|
+
spec.version = JPie::VERSION
|
8
|
+
spec.authors = ['Emil Kampp']
|
9
|
+
spec.email = ['emil@example.com']
|
10
|
+
|
11
|
+
spec.summary = 'A resource-focused Rails library for developing JSON:API compliant servers'
|
12
|
+
spec.description = 'JPie provides a framework for developing JSON:API compliant servers with Rails 8+. ' \
|
13
|
+
'It focuses on clean architecture with strong separation of concerns.'
|
14
|
+
spec.homepage = 'https://github.com/emilkampp/jpie'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.required_ruby_version = '>= 3.4.0'
|
17
|
+
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = "#{spec.homepage}.git"
|
21
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = 'exe'
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ['lib']
|
33
|
+
|
34
|
+
# Runtime dependencies
|
35
|
+
spec.add_dependency 'activesupport', '~> 8.0', '>= 8.0.0'
|
36
|
+
spec.add_dependency 'rails', '~> 8.0', '>= 8.0.0'
|
37
|
+
|
38
|
+
# Development dependencies
|
39
|
+
spec.add_development_dependency 'brakeman', '~> 6.0'
|
40
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
41
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
42
|
+
spec.add_development_dependency 'rspec-rails', '~> 7.0'
|
43
|
+
spec.add_development_dependency 'rubocop', '~> 1.50'
|
44
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.16'
|
45
|
+
spec.add_development_dependency 'rubocop-rails', '~> 2.18'
|
46
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.20'
|
47
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
48
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module CrudActions
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def jsonapi_resource(resource_class)
|
10
|
+
setup_jsonapi_resource(resource_class)
|
11
|
+
end
|
12
|
+
|
13
|
+
# More concise alias for modern Rails style
|
14
|
+
alias_method :resource, :jsonapi_resource
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def setup_jsonapi_resource(resource_class)
|
19
|
+
define_method :resource_class do
|
20
|
+
resource_class
|
21
|
+
end
|
22
|
+
|
23
|
+
# Define automatic CRUD methods
|
24
|
+
define_automatic_crud_methods(resource_class)
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_automatic_crud_methods(resource_class)
|
28
|
+
define_index_method(resource_class)
|
29
|
+
define_show_method(resource_class)
|
30
|
+
define_create_method(resource_class)
|
31
|
+
define_update_method(resource_class)
|
32
|
+
define_destroy_method(resource_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_index_method(resource_class)
|
36
|
+
define_method :index do
|
37
|
+
resources = resource_class.scope(context)
|
38
|
+
sort_fields = parse_sort_params
|
39
|
+
resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
|
40
|
+
render_jsonapi(resources)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def define_show_method(resource_class)
|
45
|
+
define_method :show do
|
46
|
+
resource = resource_class.scope(context).find(params[:id])
|
47
|
+
render_jsonapi(resource)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def define_create_method(resource_class)
|
52
|
+
define_method :create do
|
53
|
+
attributes = deserialize_params
|
54
|
+
resource = resource_class.model.create!(attributes)
|
55
|
+
render_jsonapi(resource, status: :created)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_update_method(resource_class)
|
60
|
+
define_method :update do
|
61
|
+
resource = resource_class.scope(context).find(params[:id])
|
62
|
+
attributes = deserialize_params
|
63
|
+
resource.update!(attributes)
|
64
|
+
render_jsonapi(resource)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def define_destroy_method(resource_class)
|
69
|
+
define_method :destroy do
|
70
|
+
resource = resource_class.scope(context).find(params[:id])
|
71
|
+
resource.destroy!
|
72
|
+
head :no_content
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# These methods can still be called manually or used to override defaults
|
78
|
+
def index
|
79
|
+
resources = resource_class.scope(context)
|
80
|
+
sort_fields = parse_sort_params
|
81
|
+
resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
|
82
|
+
render_jsonapi(resources)
|
83
|
+
end
|
84
|
+
|
85
|
+
def show
|
86
|
+
resource = resource_class.scope(context).find(params[:id])
|
87
|
+
render_jsonapi(resource)
|
88
|
+
end
|
89
|
+
|
90
|
+
def create
|
91
|
+
attributes = deserialize_params
|
92
|
+
resource = model_class.create!(attributes)
|
93
|
+
render_jsonapi(resource, status: :created)
|
94
|
+
end
|
95
|
+
|
96
|
+
def update
|
97
|
+
resource = resource_class.scope(context).find(params[:id])
|
98
|
+
attributes = deserialize_params
|
99
|
+
resource.update!(attributes)
|
100
|
+
render_jsonapi(resource)
|
101
|
+
end
|
102
|
+
|
103
|
+
def destroy
|
104
|
+
resource = resource_class.scope(context).find(params[:id])
|
105
|
+
resource.destroy!
|
106
|
+
head :no_content
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module ErrorHandling
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
rescue_from JPie::Errors::Error, with: :render_jsonapi_error
|
10
|
+
|
11
|
+
if defined?(ActiveRecord)
|
12
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
13
|
+
rescue_from ActiveRecord::RecordInvalid, with: :render_validation_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def render_jsonapi_error(error)
|
20
|
+
render json: { errors: [error.to_hash] },
|
21
|
+
status: error.status,
|
22
|
+
content_type: 'application/vnd.api+json'
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_not_found_error(error)
|
26
|
+
json_error = JPie::Errors::NotFoundError.new(detail: error.message)
|
27
|
+
render_jsonapi_error(json_error)
|
28
|
+
end
|
29
|
+
|
30
|
+
def render_validation_error(error)
|
31
|
+
errors = error.record.errors.full_messages.map do
|
32
|
+
JPie::Errors::ValidationError.new(detail: it).to_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
render json: { errors: },
|
36
|
+
status: :unprocessable_entity,
|
37
|
+
content_type: 'application/vnd.api+json'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module ParameterParsing
|
6
|
+
def parse_include_params
|
7
|
+
params[:include]&.split(',')&.map(&:strip) || []
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_sort_params
|
11
|
+
params[:sort]&.split(',')&.map(&:strip) || []
|
12
|
+
end
|
13
|
+
|
14
|
+
def deserialize_params
|
15
|
+
deserializer.deserialize(request.body.read, context)
|
16
|
+
rescue JSON::ParserError => e
|
17
|
+
raise JPie::Errors::BadRequestError.new(detail: "Invalid JSON: #{e.message}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def context
|
21
|
+
@context ||= build_context
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_context
|
27
|
+
{
|
28
|
+
current_user: try(:current_user),
|
29
|
+
controller: self,
|
30
|
+
action: action_name
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module Rendering
|
6
|
+
def resource_class
|
7
|
+
# Default implementation that infers from controller name
|
8
|
+
@resource_class ||= infer_resource_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def serializer
|
12
|
+
@serializer ||= JPie::Serializer.new(resource_class)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserializer
|
16
|
+
@deserializer ||= JPie::Deserializer.new(resource_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def model_class
|
22
|
+
resource_class.model
|
23
|
+
end
|
24
|
+
|
25
|
+
# More concise method names following Rails conventions
|
26
|
+
def render_jsonapi(resource_or_resources, status: :ok, meta: nil)
|
27
|
+
includes = parse_include_params
|
28
|
+
json_data = serializer.serialize(resource_or_resources, context, includes: includes)
|
29
|
+
json_data[:meta] = meta if meta
|
30
|
+
|
31
|
+
render json: json_data, status:, content_type: 'application/vnd.api+json'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Keep original methods for backward compatibility
|
35
|
+
alias render_jsonapi_resource render_jsonapi
|
36
|
+
alias render_jsonapi_resources render_jsonapi
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def infer_resource_class
|
41
|
+
# Convert controller name to resource class name
|
42
|
+
# e.g., "UsersController" -> "UserResource"
|
43
|
+
# e.g., "Api::V1::UsersController" -> "UserResource"
|
44
|
+
controller_name = self.class.name
|
45
|
+
return nil unless controller_name&.end_with?('Controller')
|
46
|
+
|
47
|
+
# Remove "Controller" suffix and any namespace
|
48
|
+
base_name = controller_name.split('::').last.chomp('Controller')
|
49
|
+
|
50
|
+
# Convert plural controller name to singular resource name
|
51
|
+
# e.g., "Users" -> "User"
|
52
|
+
singular_name = base_name.singularize
|
53
|
+
resource_class_name = "#{singular_name}Resource"
|
54
|
+
|
55
|
+
# Try to constantize the resource class
|
56
|
+
resource_class_name.constantize
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require_relative 'controller/error_handling'
|
5
|
+
require_relative 'controller/parameter_parsing'
|
6
|
+
require_relative 'controller/rendering'
|
7
|
+
require_relative 'controller/crud_actions'
|
8
|
+
|
9
|
+
module JPie
|
10
|
+
module Controller
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
include ErrorHandling
|
14
|
+
include ParameterParsing
|
15
|
+
include Rendering
|
16
|
+
include CrudActions
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
class Deserializer
|
5
|
+
attr_reader :resource_class, :options
|
6
|
+
|
7
|
+
def initialize(resource_class, options = {})
|
8
|
+
@resource_class = resource_class
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def deserialize(json_data, context = {})
|
13
|
+
data = json_data.is_a?(String) ? JSON.parse(json_data) : json_data
|
14
|
+
|
15
|
+
validate_json_api_structure!(data)
|
16
|
+
|
17
|
+
if data['data'].is_a?(Array)
|
18
|
+
deserialize_collection(data['data'], context)
|
19
|
+
else
|
20
|
+
deserialize_single(data['data'], context)
|
21
|
+
end
|
22
|
+
rescue JSON::ParserError => e
|
23
|
+
raise Errors::BadRequestError.new(detail: "Invalid JSON: #{e.message}")
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def deserialize_single(resource_data, context)
|
29
|
+
validate_resource_data!(resource_data)
|
30
|
+
extract_attributes(resource_data, context)
|
31
|
+
end
|
32
|
+
|
33
|
+
def deserialize_collection(resources_data, context)
|
34
|
+
resources_data.map { deserialize_single(it, context) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_attributes(resource_data, _context)
|
38
|
+
attributes = resource_data['attributes'] || {}
|
39
|
+
type = resource_data['type']
|
40
|
+
id = resource_data['id']
|
41
|
+
|
42
|
+
validate_type!(type) if type
|
43
|
+
|
44
|
+
model_attributes = transform_attribute_keys(attributes)
|
45
|
+
filtered_attributes = filter_allowed_attributes(model_attributes)
|
46
|
+
result = deserialize_attribute_values(filtered_attributes)
|
47
|
+
|
48
|
+
add_id_if_present(result, id)
|
49
|
+
end
|
50
|
+
|
51
|
+
def transform_attribute_keys(attributes)
|
52
|
+
attributes.transform_keys { it.to_s.underscore }
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter_allowed_attributes(model_attributes)
|
56
|
+
allowed_attributes = resource_class._attributes.map(&:to_s)
|
57
|
+
model_attributes.slice(*allowed_attributes)
|
58
|
+
end
|
59
|
+
|
60
|
+
def deserialize_attribute_values(attributes)
|
61
|
+
attributes.transform_values { deserialize_value(it) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_id_if_present(result, id)
|
65
|
+
result['id'] = id if id
|
66
|
+
result.with_indifferent_access
|
67
|
+
end
|
68
|
+
|
69
|
+
def deserialize_value(value)
|
70
|
+
case value
|
71
|
+
when String
|
72
|
+
# Only try to parse as datetime if it looks like an ISO8601 string
|
73
|
+
if value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
74
|
+
begin
|
75
|
+
Time.parse(value)
|
76
|
+
rescue ArgumentError, TypeError
|
77
|
+
value
|
78
|
+
end
|
79
|
+
else
|
80
|
+
value
|
81
|
+
end
|
82
|
+
else
|
83
|
+
value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_json_api_structure!(data)
|
88
|
+
return if data.is_a?(Hash) && data.key?('data')
|
89
|
+
|
90
|
+
raise Errors::BadRequestError.new(detail: 'Invalid JSON:API structure. Missing "data" key.')
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_resource_data!(resource_data)
|
94
|
+
raise Errors::BadRequestError.new(detail: 'Invalid resource data structure.') unless resource_data.is_a?(Hash)
|
95
|
+
|
96
|
+
return if resource_data.key?('type')
|
97
|
+
|
98
|
+
raise Errors::BadRequestError.new(detail: 'Resource data must include "type".')
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_type!(type)
|
102
|
+
expected_type = resource_class.type
|
103
|
+
return if type == expected_type
|
104
|
+
|
105
|
+
raise Errors::BadRequestError.new(
|
106
|
+
detail: "Expected type '#{expected_type}', got '#{type}'"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/jpie/errors.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :status, :code, :title, :detail, :source
|
7
|
+
|
8
|
+
def initialize(status:, code: nil, title: nil, detail: nil, source: nil)
|
9
|
+
@status = status
|
10
|
+
@code = code
|
11
|
+
@title = title
|
12
|
+
@detail = detail
|
13
|
+
@source = source
|
14
|
+
super(detail || title || 'An error occurred')
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
status: status.to_s,
|
20
|
+
code:,
|
21
|
+
title:,
|
22
|
+
detail:,
|
23
|
+
source:
|
24
|
+
}.compact
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class ValidationError < Error
|
29
|
+
def initialize(detail:, source: nil)
|
30
|
+
super(status: 422, title: 'Validation Error', detail:, source:)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NotFoundError < Error
|
35
|
+
def initialize(detail: 'Resource not found')
|
36
|
+
super(status: 404, title: 'Not Found', detail:)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class BadRequestError < Error
|
41
|
+
def initialize(detail: 'Bad Request')
|
42
|
+
super(status: 400, title: 'Bad Request', detail:)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class UnauthorizedError < Error
|
47
|
+
def initialize(detail: 'Unauthorized')
|
48
|
+
super(status: 401, title: 'Unauthorized', detail:)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class ForbiddenError < Error
|
53
|
+
def initialize(detail: 'Forbidden')
|
54
|
+
super(status: 403, title: 'Forbidden', detail:)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class InternalServerError < Error
|
59
|
+
def initialize(detail: 'Internal Server Error')
|
60
|
+
super(status: 500, title: 'Internal Server Error', detail:)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class ResourceError < Error
|
65
|
+
def initialize(detail:)
|
66
|
+
super(status: 500, title: 'Resource Error', detail:)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module JPie
|
6
|
+
module Generators
|
7
|
+
class ResourceGenerator < Rails::Generators::NamedBase
|
8
|
+
desc 'Generate a JPie resource class'
|
9
|
+
|
10
|
+
argument :attributes, type: :array, default: [], banner: 'field:type field:type'
|
11
|
+
|
12
|
+
class_option :model, type: :string, desc: 'Model class to associate with this resource'
|
13
|
+
|
14
|
+
def create_resource_file
|
15
|
+
template 'resource.rb.erb', File.join('app/resources', "#{file_name}_resource.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def model_class_name
|
21
|
+
options[:model] || class_name
|
22
|
+
end
|
23
|
+
|
24
|
+
def resource_attributes
|
25
|
+
return [] if attributes.empty?
|
26
|
+
|
27
|
+
attributes.map(&:name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def template_path
|
31
|
+
File.expand_path('templates', __dir__)
|
32
|
+
end
|
33
|
+
|
34
|
+
def source_root
|
35
|
+
template_path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %>Resource < JPie::Resource
|
4
|
+
model <%= model_class_name %>
|
5
|
+
|
6
|
+
<% if resource_attributes.any? -%>
|
7
|
+
attributes <%= resource_attributes.map { |attr| ":#{attr}" }.join(', ') %>
|
8
|
+
<% else -%>
|
9
|
+
# Define your attributes here:
|
10
|
+
# attributes :name, :email, :created_at
|
11
|
+
<% end -%>
|
12
|
+
end
|
data/lib/jpie/railtie.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
|
5
|
+
module JPie
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
railtie_name :jpie
|
8
|
+
|
9
|
+
config.jpie = ActiveSupport::OrderedOptions.new
|
10
|
+
|
11
|
+
# Configure Rails inflections to preserve JPie casing
|
12
|
+
initializer 'jpie.inflections' do
|
13
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
14
|
+
inflect.acronym 'JPie'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
initializer 'jpie.configure' do |app|
|
19
|
+
JPie.configure do |config|
|
20
|
+
app.config.jpie.each do |key, value|
|
21
|
+
config.public_send("#{key}=", value) if config.respond_to?("#{key}=")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
initializer 'jpie.action_controller' do
|
27
|
+
ActiveSupport.on_load(:action_controller) do
|
28
|
+
extend JPie::Controller::ClassMethods if defined?(JPie::Controller::ClassMethods)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
generators do
|
33
|
+
require 'jpie/generators/resource_generator'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|