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
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
class Resource
|
5
|
+
module Attributable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :_attributes, :_relationships, :_meta_attributes
|
10
|
+
self._attributes = []
|
11
|
+
self._relationships = {}
|
12
|
+
self._meta_attributes = []
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def attribute(name, options = {}, &)
|
17
|
+
name = name.to_sym
|
18
|
+
_attributes << name unless _attributes.include?(name)
|
19
|
+
define_attribute_method(name, options, &)
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes(*names)
|
23
|
+
names.each { attribute(it) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def meta_attribute(name, options = {}, &)
|
27
|
+
name = name.to_sym
|
28
|
+
_meta_attributes << name unless _meta_attributes.include?(name)
|
29
|
+
define_attribute_method(name, options, &)
|
30
|
+
end
|
31
|
+
|
32
|
+
def meta_attributes(*names)
|
33
|
+
names.each { meta_attribute(it) }
|
34
|
+
end
|
35
|
+
|
36
|
+
# More concise aliases for modern Rails style
|
37
|
+
alias_method :meta, :meta_attribute
|
38
|
+
alias_method :metas, :meta_attributes
|
39
|
+
|
40
|
+
def relationship(name, options = {})
|
41
|
+
name = name.to_sym
|
42
|
+
_relationships[name] = options
|
43
|
+
|
44
|
+
# Check if method is already defined (public or private) to allow custom implementations
|
45
|
+
return if method_defined?(name) || private_method_defined?(name)
|
46
|
+
|
47
|
+
define_method(name) do
|
48
|
+
attr_name = options[:attr] || name
|
49
|
+
@object.public_send(attr_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_many(name, options = {})
|
54
|
+
name = name.to_sym
|
55
|
+
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
56
|
+
relationship(name, { resource: resource_class_name }.merge(options))
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_one(name, options = {})
|
60
|
+
name = name.to_sym
|
61
|
+
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
62
|
+
relationship(name, { resource: resource_class_name }.merge(options))
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def define_attribute_method(name, options, &)
|
68
|
+
# If a block is provided, use it (existing behavior)
|
69
|
+
if block_given?
|
70
|
+
define_method(name) do
|
71
|
+
instance_exec(&)
|
72
|
+
end
|
73
|
+
# If options[:block] is provided, use it (existing behavior)
|
74
|
+
elsif options[:block]
|
75
|
+
define_method(name) do
|
76
|
+
instance_exec(&options[:block])
|
77
|
+
end
|
78
|
+
# If method is not already defined on the resource (public or private), define the default implementation
|
79
|
+
elsif !method_defined?(name) && !private_method_defined?(name)
|
80
|
+
define_method(name) do
|
81
|
+
attr_name = options[:attr] || name
|
82
|
+
@object.public_send(attr_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
# If method is already defined, don't override it - let the custom method handle it
|
86
|
+
end
|
87
|
+
|
88
|
+
def infer_resource_class_name(relationship_name)
|
89
|
+
# Convert relationship name to resource class name
|
90
|
+
# :posts -> "PostResource"
|
91
|
+
# :user -> "UserResource"
|
92
|
+
singularized_name = relationship_name.to_s.singularize
|
93
|
+
"#{singularized_name.camelize}Resource"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
class Resource
|
5
|
+
module Inferrable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :_type, :_model_class
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def model(model_class = nil)
|
14
|
+
if model_class
|
15
|
+
self._model_class = model_class
|
16
|
+
else
|
17
|
+
_model_class || infer_model_class
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def type(type_name = nil)
|
22
|
+
if type_name
|
23
|
+
self._type = type_name.to_s
|
24
|
+
else
|
25
|
+
_type || infer_type_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def infer_model_class
|
32
|
+
name.chomp('Resource').constantize
|
33
|
+
rescue NameError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def infer_type_name
|
38
|
+
model&.model_name&.plural || name.chomp('Resource').underscore.pluralize
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
class Resource
|
5
|
+
module Sortable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :_sortable_fields
|
10
|
+
self._sortable_fields = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
# Define a sortable field with optional custom sorting logic
|
15
|
+
# Example:
|
16
|
+
# sortable_by :name
|
17
|
+
# sortable_by :created_at, :created_at_desc
|
18
|
+
# sortable_by :popularity do |direction|
|
19
|
+
# if direction == :asc
|
20
|
+
# model.order(:likes_count)
|
21
|
+
# else
|
22
|
+
# model.order(likes_count: :desc)
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
def sortable_by(field, column = nil, &block)
|
26
|
+
field = field.to_sym
|
27
|
+
_sortable_fields[field] = block || column || field
|
28
|
+
end
|
29
|
+
|
30
|
+
# More concise alias for modern Rails style
|
31
|
+
alias_method :sortable, :sortable_by
|
32
|
+
|
33
|
+
# Apply sorting to a query based on sort parameters
|
34
|
+
# sort_fields: array of sort field strings (e.g., ['name', '-created_at'])
|
35
|
+
def sort(query, sort_fields)
|
36
|
+
return query if sort_fields.blank?
|
37
|
+
|
38
|
+
sort_fields.each do |sort_field|
|
39
|
+
# Parse direction (- prefix means descending)
|
40
|
+
if sort_field.start_with?('-')
|
41
|
+
field = sort_field[1..].to_sym
|
42
|
+
direction = :desc
|
43
|
+
else
|
44
|
+
field = sort_field.to_sym
|
45
|
+
direction = :asc
|
46
|
+
end
|
47
|
+
|
48
|
+
# Check if field is sortable
|
49
|
+
unless sortable_field?(field)
|
50
|
+
raise JPie::Errors::BadRequestError.new(
|
51
|
+
detail: "Invalid sort field: #{field}. Sortable fields are: #{sortable_fields.join(', ')}"
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Apply sorting
|
56
|
+
query = apply_sort(query, field, direction)
|
57
|
+
end
|
58
|
+
|
59
|
+
query
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get list of all sortable fields (attributes + explicitly defined sortable fields)
|
63
|
+
def sortable_fields
|
64
|
+
(_attributes + _sortable_fields.keys).uniq.map(&:to_s)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if a field is sortable
|
68
|
+
def sortable_field?(field)
|
69
|
+
field = field.to_sym
|
70
|
+
_attributes.include?(field) || _sortable_fields.key?(field)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Apply a single sort to the query
|
76
|
+
def apply_sort(query, field, direction)
|
77
|
+
sortable_config = _sortable_fields[field]
|
78
|
+
|
79
|
+
if sortable_config.is_a?(Proc)
|
80
|
+
# Custom sorting block
|
81
|
+
instance_exec(query, direction, &sortable_config)
|
82
|
+
elsif sortable_config.is_a?(Symbol)
|
83
|
+
# Custom column name
|
84
|
+
query.order(sortable_config => direction)
|
85
|
+
else
|
86
|
+
# Default sorting by attribute name
|
87
|
+
query.order(field => direction)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'resource/attributable'
|
4
|
+
require_relative 'resource/inferrable'
|
5
|
+
require_relative 'resource/sortable'
|
6
|
+
require_relative 'errors'
|
7
|
+
|
8
|
+
module JPie
|
9
|
+
class Resource
|
10
|
+
include ActiveSupport::Configurable
|
11
|
+
include Attributable
|
12
|
+
include Inferrable
|
13
|
+
include Sortable
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def inherited(subclass)
|
17
|
+
super
|
18
|
+
subclass._attributes = _attributes.dup
|
19
|
+
subclass._relationships = _relationships.dup
|
20
|
+
subclass._meta_attributes = _meta_attributes.dup
|
21
|
+
subclass._sortable_fields = _sortable_fields.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# Default scope method that returns all records
|
25
|
+
# Override this in your resource classes to provide authorization scoping
|
26
|
+
# Example:
|
27
|
+
# def self.scope(context)
|
28
|
+
# current_user = context[:current_user]
|
29
|
+
# return model.none unless current_user
|
30
|
+
#
|
31
|
+
# if current_user.admin?
|
32
|
+
# model.all
|
33
|
+
# else
|
34
|
+
# model.where(user: current_user)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
def scope(_context = {})
|
38
|
+
model.all
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :object, :context
|
43
|
+
|
44
|
+
def initialize(object, context = {})
|
45
|
+
@object = object
|
46
|
+
@context = context
|
47
|
+
end
|
48
|
+
|
49
|
+
delegate :id, to: :@object
|
50
|
+
|
51
|
+
delegate :type, to: :class
|
52
|
+
|
53
|
+
def attributes_hash
|
54
|
+
self.class._attributes.index_with do
|
55
|
+
send(it)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def meta_hash
|
60
|
+
# Start with meta attributes from the macro
|
61
|
+
base_meta = self.class._meta_attributes.index_with do
|
62
|
+
send(it)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if the resource defines a custom meta method
|
66
|
+
if respond_to?(:meta, true) && method(:meta).owner != JPie::Resource
|
67
|
+
custom_meta = meta
|
68
|
+
|
69
|
+
# Validate that meta method returns a hash
|
70
|
+
unless custom_meta.is_a?(Hash)
|
71
|
+
raise JPie::Errors::ResourceError.new(
|
72
|
+
detail: "meta method must return a Hash, got #{custom_meta.class}"
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Merge custom meta with base meta (custom meta takes precedence)
|
77
|
+
base_meta.merge(custom_meta)
|
78
|
+
else
|
79
|
+
base_meta
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# Default meta method that returns the meta attributes
|
86
|
+
# This can be overridden in subclasses
|
87
|
+
def meta
|
88
|
+
self.class._meta_attributes.index_with do
|
89
|
+
send(it)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def method_missing(method_name, *, &)
|
96
|
+
if @object.respond_to?(method_name)
|
97
|
+
@object.public_send(method_name, *, &)
|
98
|
+
else
|
99
|
+
super
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def respond_to_missing?(method_name, include_private = false)
|
104
|
+
@object.respond_to?(method_name, include_private) || super
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
class Serializer
|
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 serialize(objects, context = {}, includes: [])
|
13
|
+
return { data: nil } if objects.nil?
|
14
|
+
|
15
|
+
resources = build_resources(objects, context)
|
16
|
+
result = serialize_data(objects, resources)
|
17
|
+
add_included_data(result, resources, includes, context) if should_include_data?(includes, result)
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def build_resources(objects, context)
|
24
|
+
Array(objects).filter_map { |obj| obj ? resource_class.new(obj, context) : nil }
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize_data(objects, resources)
|
28
|
+
if objects.is_a?(Array) || objects.respond_to?(:each)
|
29
|
+
serialize_collection(resources)
|
30
|
+
else
|
31
|
+
resources.first ? serialize_single(resources.first) : { data: nil }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def should_include_data?(includes, result)
|
36
|
+
includes.any? && result[:data]
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_included_data(result, resources, includes, context)
|
40
|
+
included_data = collect_included_data(resources, includes, context)
|
41
|
+
result[:included] = included_data
|
42
|
+
end
|
43
|
+
|
44
|
+
def serialize_single(resource)
|
45
|
+
{
|
46
|
+
data: serialize_resource_data(resource)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def serialize_collection(resources)
|
51
|
+
{
|
52
|
+
data: resources.map { serialize_resource_data(it) }
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def serialize_resource_data(resource)
|
57
|
+
data = {
|
58
|
+
id: resource.id.to_s,
|
59
|
+
type: resource.type,
|
60
|
+
attributes: serialize_attributes(resource)
|
61
|
+
}
|
62
|
+
|
63
|
+
meta_data = serialize_meta(resource)
|
64
|
+
data[:meta] = meta_data if meta_data.any?
|
65
|
+
|
66
|
+
data.compact
|
67
|
+
end
|
68
|
+
|
69
|
+
def serialize_attributes(resource)
|
70
|
+
attributes = resource.attributes_hash
|
71
|
+
return {} if attributes.empty?
|
72
|
+
|
73
|
+
attributes.transform_keys { it.to_s.underscore }
|
74
|
+
.transform_values { serialize_value(it) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def serialize_meta(resource)
|
78
|
+
meta_attributes = resource.meta_hash
|
79
|
+
return {} if meta_attributes.empty?
|
80
|
+
|
81
|
+
meta_attributes.transform_keys { it.to_s.underscore }
|
82
|
+
.transform_values { serialize_value(it) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def serialize_value(value)
|
86
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value
|
87
|
+
end
|
88
|
+
|
89
|
+
def collect_included_data(resources, includes, context)
|
90
|
+
processor = IncludeProcessor.new(self, context)
|
91
|
+
processor.process(resources, includes)
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_nested_includes(includes)
|
95
|
+
result = {}
|
96
|
+
|
97
|
+
includes.each do |include_path|
|
98
|
+
parts = include_path.split('.')
|
99
|
+
top_level = parts.first
|
100
|
+
nested_path = parts[1..].join('.') if parts.length > 1
|
101
|
+
|
102
|
+
result[top_level] ||= []
|
103
|
+
result[top_level] << nested_path if nested_path.present?
|
104
|
+
end
|
105
|
+
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
# Helper class to manage include processing state and reduce parameter passing
|
110
|
+
class IncludeProcessor
|
111
|
+
attr_reader :serializer, :context, :included, :processed_includes
|
112
|
+
|
113
|
+
def initialize(serializer, context)
|
114
|
+
@serializer = serializer
|
115
|
+
@context = context
|
116
|
+
@included = []
|
117
|
+
@processed_includes = {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def process(resources, includes)
|
121
|
+
parsed_includes = serializer.send(:parse_nested_includes, includes)
|
122
|
+
parsed_includes.each do |include_name, nested_includes|
|
123
|
+
process_single_include(include_name, nested_includes, resources)
|
124
|
+
end
|
125
|
+
included
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def process_single_include(include_name, nested_includes, resources)
|
131
|
+
include_sym = include_name.to_sym
|
132
|
+
relationship_options = serializer.resource_class._relationships[include_sym]
|
133
|
+
return unless relationship_options
|
134
|
+
|
135
|
+
resources.each do |resource|
|
136
|
+
process_resource_relationships(resource, include_sym, relationship_options, nested_includes)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def process_resource_relationships(resource, include_sym, relationship_options, nested_includes)
|
141
|
+
related_objects = resource.public_send(include_sym)
|
142
|
+
return unless related_objects
|
143
|
+
|
144
|
+
Array(related_objects).each do |related_object|
|
145
|
+
process_related_object(related_object, relationship_options, nested_includes)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def process_related_object(related_object, relationship_options, nested_includes)
|
150
|
+
related_resource_class = serializer.send(:determine_resource_class, related_object, relationship_options)
|
151
|
+
return unless related_resource_class
|
152
|
+
|
153
|
+
related_resource = related_resource_class.new(related_object, context)
|
154
|
+
resource_data = serializer.send(:serialize_resource_data, related_resource)
|
155
|
+
|
156
|
+
add_to_included_if_unique(resource_data)
|
157
|
+
process_nested_includes_for_resource(related_resource_class, related_resource, nested_includes)
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_to_included_if_unique(resource_data)
|
161
|
+
key = [resource_data[:type], resource_data[:id]]
|
162
|
+
return if processed_includes[key]
|
163
|
+
|
164
|
+
processed_includes[key] = true
|
165
|
+
included << resource_data
|
166
|
+
end
|
167
|
+
|
168
|
+
def process_nested_includes_for_resource(related_resource_class, related_resource, nested_includes)
|
169
|
+
return unless nested_includes.any?
|
170
|
+
|
171
|
+
nested_serializer = JPie::Serializer.new(related_resource_class)
|
172
|
+
nested_included = nested_serializer.send(:collect_included_data, [related_resource], nested_includes, context)
|
173
|
+
|
174
|
+
nested_included.each do |nested_item|
|
175
|
+
add_to_included_if_unique(nested_item)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def determine_resource_class(object, relationship_options)
|
181
|
+
# First try the explicitly specified resource class
|
182
|
+
if relationship_options[:resource]
|
183
|
+
begin
|
184
|
+
return relationship_options[:resource].constantize
|
185
|
+
rescue NameError
|
186
|
+
# If the resource class doesn't exist, it might be a polymorphic relationship
|
187
|
+
# Fall through to polymorphic detection
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# For polymorphic relationships, determine resource class from object class
|
192
|
+
if object&.class
|
193
|
+
resource_class_name = "#{object.class.name}Resource"
|
194
|
+
begin
|
195
|
+
return resource_class_name.constantize
|
196
|
+
rescue NameError
|
197
|
+
# Resource class doesn't exist for this object type
|
198
|
+
return nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
data/lib/jpie/version.rb
ADDED
data/lib/jpie.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'jpie/version'
|
6
|
+
|
7
|
+
module JPie
|
8
|
+
autoload :Resource, 'jpie/resource'
|
9
|
+
autoload :Serializer, 'jpie/serializer'
|
10
|
+
autoload :Deserializer, 'jpie/deserializer'
|
11
|
+
autoload :Controller, 'jpie/controller'
|
12
|
+
autoload :Configuration, 'jpie/configuration'
|
13
|
+
autoload :Errors, 'jpie/errors'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure
|
21
|
+
yield(configuration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'jpie/railtie' if defined?(Rails)
|