bunko 0.2.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/.standard.yml +3 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +351 -0
- data/LICENSE.txt +21 -0
- data/README.md +641 -0
- data/ROADMAP.md +519 -0
- data/Rakefile +10 -0
- data/lib/bunko/configuration.rb +180 -0
- data/lib/bunko/controllers/acts_as.rb +22 -0
- data/lib/bunko/controllers/collection.rb +160 -0
- data/lib/bunko/controllers.rb +5 -0
- data/lib/bunko/models/acts_as.rb +24 -0
- data/lib/bunko/models/post_methods/publishable.rb +51 -0
- data/lib/bunko/models/post_methods/sluggable.rb +47 -0
- data/lib/bunko/models/post_methods/word_countable.rb +76 -0
- data/lib/bunko/models/post_methods.rb +75 -0
- data/lib/bunko/models/post_type_methods.rb +18 -0
- data/lib/bunko/models.rb +6 -0
- data/lib/bunko/railtie.rb +22 -0
- data/lib/bunko/routing/mapper_methods.rb +103 -0
- data/lib/bunko/routing.rb +4 -0
- data/lib/bunko/version.rb +5 -0
- data/lib/bunko.rb +11 -0
- data/lib/tasks/bunko/add.rake +259 -0
- data/lib/tasks/bunko/helpers.rb +25 -0
- data/lib/tasks/bunko/install.rake +125 -0
- data/lib/tasks/bunko/sample_data.rake +128 -0
- data/lib/tasks/bunko/setup.rake +186 -0
- data/lib/tasks/support/sample_data_generator.rb +399 -0
- data/lib/tasks/templates/INSTALL.md +62 -0
- data/lib/tasks/templates/config/initializers/bunko.rb.tt +45 -0
- data/lib/tasks/templates/controllers/controller.rb.tt +25 -0
- data/lib/tasks/templates/controllers/pages_controller.rb.tt +29 -0
- data/lib/tasks/templates/db/migrate/create_post_types.rb.tt +14 -0
- data/lib/tasks/templates/db/migrate/create_posts.rb.tt +31 -0
- data/lib/tasks/templates/models/post.rb.tt +8 -0
- data/lib/tasks/templates/models/post_type.rb.tt +8 -0
- data/lib/tasks/templates/views/collections/index.html.erb.tt +67 -0
- data/lib/tasks/templates/views/collections/show.html.erb.tt +39 -0
- data/lib/tasks/templates/views/layouts/bunko_footer.html.erb.tt +3 -0
- data/lib/tasks/templates/views/layouts/bunko_nav.html.erb.tt +9 -0
- data/lib/tasks/templates/views/layouts/bunko_styles.html.erb.tt +3 -0
- data/lib/tasks/templates/views/pages/show.html.erb.tt +16 -0
- data/sig/bunko.rbs +4 -0
- metadata +116 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Controllers
|
|
5
|
+
module ActsAs
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def bunko_collection(collection_name, **options)
|
|
10
|
+
include Bunko::Controllers::Collection
|
|
11
|
+
|
|
12
|
+
bunko_collection(collection_name, **options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Extend ActionController::Base with bunko_collection method
|
|
20
|
+
if defined?(ActionController::Base)
|
|
21
|
+
ActionController::Base.include Bunko::Controllers::ActsAs
|
|
22
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Controllers
|
|
5
|
+
module Collection
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
class_attribute :bunko_collection_name
|
|
10
|
+
class_attribute :bunko_collection_options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def bunko_collection(collection_name, **options)
|
|
15
|
+
self.bunko_collection_name = collection_name.to_s
|
|
16
|
+
self.bunko_collection_options = {
|
|
17
|
+
per_page: 10,
|
|
18
|
+
order: :published_at_desc
|
|
19
|
+
}.merge(options)
|
|
20
|
+
|
|
21
|
+
# Set layout if specified
|
|
22
|
+
layout(options[:layout]) if options[:layout]
|
|
23
|
+
|
|
24
|
+
# Define index and show actions
|
|
25
|
+
define_method :index do
|
|
26
|
+
load_collection
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
define_method :show do
|
|
30
|
+
load_post
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Make helpers available
|
|
34
|
+
helper_method :collection_name if respond_to?(:helper_method)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def load_collection
|
|
41
|
+
@collection_name = bunko_collection_name
|
|
42
|
+
|
|
43
|
+
# Smart lookup: Check PostType first, then Collection
|
|
44
|
+
post_type_config = Bunko.configuration.find_post_type(@collection_name)
|
|
45
|
+
collection_config = Bunko.configuration.find_collection(@collection_name)
|
|
46
|
+
|
|
47
|
+
if post_type_config
|
|
48
|
+
# Single PostType collection
|
|
49
|
+
@post_type = PostType.find_by(name: @collection_name)
|
|
50
|
+
unless @post_type
|
|
51
|
+
render plain: "PostType '#{@collection_name}' not found in database. Run: rails bunko:setup[#{@collection_name}]", status: :not_found
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
base_query = post_model.published.by_post_type(@collection_name)
|
|
56
|
+
elsif collection_config
|
|
57
|
+
# Multi-type collection
|
|
58
|
+
base_query = post_model.published.where(post_type: PostType.where(name: collection_config[:post_types]))
|
|
59
|
+
|
|
60
|
+
# Apply collection scope if defined
|
|
61
|
+
if collection_config[:scope]
|
|
62
|
+
base_query = base_query.instance_exec(&collection_config[:scope])
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
render plain: "Collection '#{@collection_name}' not found. Add it to config/initializers/bunko.rb", status: :not_found
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Apply ordering
|
|
70
|
+
ordered_query = apply_ordering(base_query)
|
|
71
|
+
|
|
72
|
+
# Apply pagination
|
|
73
|
+
@posts = paginate(ordered_query)
|
|
74
|
+
@pagination = pagination_metadata
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def load_post
|
|
78
|
+
@collection_name = bunko_collection_name
|
|
79
|
+
|
|
80
|
+
# Smart lookup: Check PostType first, then Collection
|
|
81
|
+
post_type_config = Bunko.configuration.find_post_type(@collection_name)
|
|
82
|
+
collection_config = Bunko.configuration.find_collection(@collection_name)
|
|
83
|
+
|
|
84
|
+
# Collections should not have show routes (posts are accessed via their canonical PostType URL)
|
|
85
|
+
if collection_config
|
|
86
|
+
render plain: "Posts in this collection can only be accessed through their PostType URL. This collection only supports index.", status: :not_found
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if post_type_config
|
|
91
|
+
# Single PostType collection
|
|
92
|
+
@post_type = PostType.find_by(name: @collection_name)
|
|
93
|
+
unless @post_type
|
|
94
|
+
render plain: "PostType '#{@collection_name}' not found in database. Run: rails bunko:setup[#{@collection_name}]", status: :not_found
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
base_query = post_model.published.by_post_type(@collection_name)
|
|
99
|
+
else
|
|
100
|
+
render plain: "Collection '#{@collection_name}' not found. Add it to config/initializers/bunko.rb", status: :not_found
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Find post by slug within this collection
|
|
105
|
+
@post = base_query.find_by(slug: params[:slug])
|
|
106
|
+
|
|
107
|
+
unless @post
|
|
108
|
+
render plain: "Post not found", status: :not_found
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def post_model
|
|
113
|
+
@post_model ||= Post
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def apply_ordering(query)
|
|
117
|
+
case bunko_collection_options[:order]
|
|
118
|
+
when :published_at_desc
|
|
119
|
+
query.reorder(published_at: :desc)
|
|
120
|
+
when :published_at_asc
|
|
121
|
+
query.reorder(published_at: :asc)
|
|
122
|
+
when :created_at_desc
|
|
123
|
+
query.reorder(created_at: :desc)
|
|
124
|
+
when :created_at_asc
|
|
125
|
+
query.reorder(created_at: :asc)
|
|
126
|
+
else
|
|
127
|
+
query
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def paginate(query)
|
|
132
|
+
page_number = [params[:page].to_i, 1].max
|
|
133
|
+
per_page = bunko_collection_options[:per_page]
|
|
134
|
+
|
|
135
|
+
offset = (page_number - 1) * per_page
|
|
136
|
+
|
|
137
|
+
@_total_count = query.count
|
|
138
|
+
@_current_page = page_number
|
|
139
|
+
@_per_page = per_page
|
|
140
|
+
|
|
141
|
+
query.limit(per_page).offset(offset)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def pagination_metadata
|
|
145
|
+
{
|
|
146
|
+
current_page: @_current_page,
|
|
147
|
+
per_page: @_per_page,
|
|
148
|
+
total_count: @_total_count,
|
|
149
|
+
total_pages: (@_total_count.to_f / @_per_page).ceil,
|
|
150
|
+
prev_page: (@_current_page > 1) ? @_current_page - 1 : nil,
|
|
151
|
+
next_page: (@_current_page < (@_total_count.to_f / @_per_page).ceil) ? @_current_page + 1 : nil
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def collection_name
|
|
156
|
+
@collection_name
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Models
|
|
5
|
+
module ActsAs
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def acts_as_bunko_post
|
|
10
|
+
include Bunko::Models::PostMethods
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def acts_as_bunko_post_type
|
|
14
|
+
include Bunko::Models::PostTypeMethods
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Extend ActiveRecord::Base with acts_as methods
|
|
22
|
+
if defined?(ActiveRecord::Base)
|
|
23
|
+
ActiveRecord::Base.include Bunko::Models::ActsAs
|
|
24
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Models
|
|
5
|
+
module PostMethods
|
|
6
|
+
module Publishable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
# Validations
|
|
11
|
+
validates :status, presence: true, inclusion: {
|
|
12
|
+
in: ->(_) { Bunko.configuration.valid_statuses },
|
|
13
|
+
message: "%{value} is not a valid status"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Callbacks
|
|
17
|
+
before_validation :set_published_at, if: :should_set_published_at?
|
|
18
|
+
validate :validate_status_value
|
|
19
|
+
|
|
20
|
+
# Scopes
|
|
21
|
+
scope :published, -> { where(status: "published").where("published_at <= ?", Time.current).order(published_at: :desc) }
|
|
22
|
+
scope :draft, -> { where(status: "draft").order(created_at: :desc) }
|
|
23
|
+
scope :scheduled, -> { where(status: "published").where("published_at > ?", Time.current).order(published_at: :asc) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Instance method to check if post is scheduled for future publication
|
|
27
|
+
def scheduled?
|
|
28
|
+
status == "published" && published_at.present? && published_at > Time.current
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def should_set_published_at?
|
|
34
|
+
status == "published" && published_at.blank?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set_published_at
|
|
38
|
+
self.published_at = Time.current
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_status_value
|
|
42
|
+
return if status.blank?
|
|
43
|
+
|
|
44
|
+
unless Bunko.configuration.valid_statuses.include?(status)
|
|
45
|
+
raise ArgumentError, "#{status} is not a valid status"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Models
|
|
5
|
+
module PostMethods
|
|
6
|
+
module Sluggable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
before_validation :generate_slug, if: :should_generate_slug?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def should_generate_slug?
|
|
16
|
+
slug.blank? && title.present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_slug
|
|
20
|
+
return if title.blank?
|
|
21
|
+
|
|
22
|
+
# Generate slug using parameterize, then normalize:
|
|
23
|
+
# 1. Convert underscores to hyphens (parameterize keeps them in Rails 8+)
|
|
24
|
+
# 2. Remove consecutive hyphens
|
|
25
|
+
# 3. Remove leading/trailing hyphens or underscores
|
|
26
|
+
base_slug = title.parameterize
|
|
27
|
+
.tr("_", "-").squeeze("-")
|
|
28
|
+
.gsub(/^[-_]+|[-_]+$/, "")
|
|
29
|
+
|
|
30
|
+
# Skip if slug would be empty (e.g., title with only non-Latin characters)
|
|
31
|
+
return if base_slug.blank?
|
|
32
|
+
|
|
33
|
+
self.slug = base_slug
|
|
34
|
+
|
|
35
|
+
# Ensure uniqueness within post_type
|
|
36
|
+
return unless self.class.unscoped.where(
|
|
37
|
+
post_type_id: post_type_id,
|
|
38
|
+
slug: slug
|
|
39
|
+
).where.not(id: id).exists?
|
|
40
|
+
|
|
41
|
+
# Add random suffix if slug exists
|
|
42
|
+
self.slug = "#{base_slug}-#{SecureRandom.hex(4)}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Models
|
|
5
|
+
module PostMethods
|
|
6
|
+
module WordCountable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
before_save :update_word_count, if: :should_update_word_count?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Instance methods
|
|
14
|
+
def reading_time
|
|
15
|
+
return nil unless word_count.present? && word_count > 0
|
|
16
|
+
|
|
17
|
+
(word_count.to_f / Bunko.configuration.reading_speed).ceil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reading_time_text
|
|
21
|
+
return nil unless reading_time.present?
|
|
22
|
+
|
|
23
|
+
"#{reading_time} min read"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def should_update_word_count?
|
|
29
|
+
# Only update word_count if:
|
|
30
|
+
# 1. Auto-update is enabled in config
|
|
31
|
+
# 2. Content changed
|
|
32
|
+
# 3. Model has word_count attribute
|
|
33
|
+
Bunko.configuration.auto_update_word_count &&
|
|
34
|
+
content_changed? &&
|
|
35
|
+
respond_to?(:word_count=)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update_word_count
|
|
39
|
+
if content.blank?
|
|
40
|
+
self.word_count = 0
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if content is a text field or JSON field
|
|
45
|
+
column = self.class.columns_hash["content"]
|
|
46
|
+
|
|
47
|
+
if column && [:json, :jsonb].include?(column.type)
|
|
48
|
+
# For JSON content, try to extract text recursively
|
|
49
|
+
self.word_count = count_words_in_json(content)
|
|
50
|
+
else
|
|
51
|
+
# For text content, strip HTML tags and count words
|
|
52
|
+
text = content.to_s.gsub(/<[^>]*>/, "")
|
|
53
|
+
self.word_count = text.split(/\s+/).count(&:present?)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def count_words_in_json(data)
|
|
58
|
+
case data
|
|
59
|
+
when String
|
|
60
|
+
# Strip HTML and count words in string
|
|
61
|
+
text = data.gsub(/<[^>]*>/, "")
|
|
62
|
+
text.split(/\s+/).count(&:present?)
|
|
63
|
+
when Hash
|
|
64
|
+
# Recursively count words in hash values
|
|
65
|
+
data.values.sum { |value| count_words_in_json(value) }
|
|
66
|
+
when Array
|
|
67
|
+
# Recursively count words in array elements
|
|
68
|
+
data.sum { |element| count_words_in_json(element) }
|
|
69
|
+
else
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "post_methods/sluggable"
|
|
4
|
+
require_relative "post_methods/word_countable"
|
|
5
|
+
require_relative "post_methods/publishable"
|
|
6
|
+
|
|
7
|
+
module Bunko
|
|
8
|
+
module Models
|
|
9
|
+
module PostMethods
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
include Sluggable
|
|
13
|
+
include WordCountable
|
|
14
|
+
include Publishable
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
# Associations
|
|
18
|
+
belongs_to :post_type
|
|
19
|
+
|
|
20
|
+
# Validations
|
|
21
|
+
validates :title, presence: true
|
|
22
|
+
validates :slug,
|
|
23
|
+
presence: true,
|
|
24
|
+
uniqueness: {scope: :post_type_id},
|
|
25
|
+
format: {
|
|
26
|
+
with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/,
|
|
27
|
+
message: "must contain only lowercase letters, numbers, and hyphens"
|
|
28
|
+
},
|
|
29
|
+
length: {maximum: 255}
|
|
30
|
+
|
|
31
|
+
# Default scope for ordering
|
|
32
|
+
default_scope { order(created_at: :desc) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class_methods do
|
|
36
|
+
def by_post_type(type_name)
|
|
37
|
+
joins(:post_type).where(post_types: {name: type_name})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Instance methods
|
|
42
|
+
def to_param
|
|
43
|
+
slug
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def excerpt(length: nil, omission: "...")
|
|
47
|
+
return nil unless content.present?
|
|
48
|
+
|
|
49
|
+
# Use configured default if length not specified
|
|
50
|
+
length ||= Bunko.configuration.excerpt_length
|
|
51
|
+
|
|
52
|
+
# Strip HTML tags using Rails sanitizer (more robust than regex)
|
|
53
|
+
text = ActionView::Base.full_sanitizer.sanitize(content.to_s)
|
|
54
|
+
|
|
55
|
+
# Clean up extra whitespace
|
|
56
|
+
text = text.gsub(/\s+/, " ").strip
|
|
57
|
+
|
|
58
|
+
# Return full text if shorter than length
|
|
59
|
+
return text if text.length <= length
|
|
60
|
+
|
|
61
|
+
# Truncate to word boundary
|
|
62
|
+
truncated = text[0...length]
|
|
63
|
+
last_space = truncated.rindex(" ") || length
|
|
64
|
+
|
|
65
|
+
"#{truncated[0...last_space]}#{omission}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def published_date(format = :long)
|
|
69
|
+
return nil unless published_at.present?
|
|
70
|
+
|
|
71
|
+
I18n.l(published_at, format: format)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Models
|
|
5
|
+
module PostTypeMethods
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# Associations
|
|
10
|
+
has_many :posts, dependent: :restrict_with_error
|
|
11
|
+
|
|
12
|
+
# Validations
|
|
13
|
+
validates :name, presence: true, uniqueness: true
|
|
14
|
+
validates :title, presence: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/bunko/models.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Bunko
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
# Extend Rails routing DSL with bunko_collection
|
|
8
|
+
initializer "bunko.routing" do
|
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
|
10
|
+
require "bunko/routing"
|
|
11
|
+
ActionDispatch::Routing::Mapper.include Bunko::Routing::MapperMethods
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rake_tasks do
|
|
16
|
+
load "tasks/bunko/install.rake"
|
|
17
|
+
load "tasks/bunko/setup.rake"
|
|
18
|
+
load "tasks/bunko/add.rake"
|
|
19
|
+
load "tasks/bunko/sample_data.rake"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
module Routing
|
|
5
|
+
module MapperMethods
|
|
6
|
+
# Defines routes for a Bunko collection
|
|
7
|
+
#
|
|
8
|
+
# @param collection_name [Symbol] The name identifier for the collection (e.g., :blog, :case_study)
|
|
9
|
+
# @param options [Hash] Routing options
|
|
10
|
+
# @option options [String] :path Custom URL path (default: name with hyphens)
|
|
11
|
+
# @option options [String] :controller Custom controller name (default: name)
|
|
12
|
+
# @option options [Array<Symbol>] :only Actions to route (default: [:index, :show])
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# bunko_collection :blog
|
|
16
|
+
# # Generates: /blog -> blog#index, /blog/:slug -> blog#show
|
|
17
|
+
#
|
|
18
|
+
# @example Custom path
|
|
19
|
+
# bunko_collection :case_study, path: "case-studies"
|
|
20
|
+
# # Generates: /case-studies -> case_study#index, /case-studies/:slug -> case_study#show
|
|
21
|
+
#
|
|
22
|
+
# @example Custom controller
|
|
23
|
+
# bunko_collection :blog, controller: "articles"
|
|
24
|
+
# # Generates: /blog -> articles#index, /blog/:slug -> articles#show
|
|
25
|
+
#
|
|
26
|
+
def bunko_collection(collection_name, **options)
|
|
27
|
+
# Extract options with defaults
|
|
28
|
+
custom_path = options.delete(:path)
|
|
29
|
+
controller = options.delete(:controller) || collection_name.to_s
|
|
30
|
+
|
|
31
|
+
# Smart detection: Collections (multi-type aggregations) only get index routes
|
|
32
|
+
# PostTypes get both index and show routes
|
|
33
|
+
collection_config = Bunko.configuration.find_collection(collection_name.to_s)
|
|
34
|
+
|
|
35
|
+
# Default actions: Collections get [:index], PostTypes get [:index, :show]
|
|
36
|
+
# Users can override with :only option
|
|
37
|
+
default_actions = collection_config ? [:index] : [:index, :show]
|
|
38
|
+
actions = options.delete(:only) || default_actions
|
|
39
|
+
|
|
40
|
+
# Resource name must use underscores (for path helpers)
|
|
41
|
+
# Path can use hyphens (for URLs)
|
|
42
|
+
if custom_path
|
|
43
|
+
# User provided custom path - use it for URLs, underscored version for resource name
|
|
44
|
+
resource_name = custom_path.to_s.tr("-", "_").to_sym
|
|
45
|
+
path_value = custom_path
|
|
46
|
+
else
|
|
47
|
+
# No custom path - use collection_name for resource name, hyphenate for path
|
|
48
|
+
resource_name = collection_name
|
|
49
|
+
path_value = collection_name.to_s.dasherize
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Define the routes
|
|
53
|
+
resources resource_name,
|
|
54
|
+
controller: controller,
|
|
55
|
+
path: path_value,
|
|
56
|
+
only: actions,
|
|
57
|
+
param: :slug,
|
|
58
|
+
**options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Defines a route for a standalone Bunko page
|
|
62
|
+
#
|
|
63
|
+
# @param page_name [Symbol] The name identifier for the page (e.g., :about, :contact)
|
|
64
|
+
# @param options [Hash] Routing options
|
|
65
|
+
# @option options [String] :path Custom URL path (default: name with hyphens)
|
|
66
|
+
# @option options [String] :controller Custom controller name (default: "pages")
|
|
67
|
+
#
|
|
68
|
+
# @example Basic usage
|
|
69
|
+
# bunko_page :about
|
|
70
|
+
# # Generates: GET /about -> pages#show with params[:page] = "about"
|
|
71
|
+
#
|
|
72
|
+
# @example Custom path
|
|
73
|
+
# bunko_page :about, path: "about-us"
|
|
74
|
+
# # Generates: GET /about-us -> pages#show with params[:page] = "about"
|
|
75
|
+
#
|
|
76
|
+
# @example Custom controller
|
|
77
|
+
# bunko_page :contact, controller: "static_pages"
|
|
78
|
+
# # Generates: GET /contact -> static_pages#show with params[:page] = "contact"
|
|
79
|
+
#
|
|
80
|
+
def bunko_page(page_name, **options)
|
|
81
|
+
# Extract options with defaults
|
|
82
|
+
custom_path = options.delete(:path)
|
|
83
|
+
controller = options.delete(:controller) || "pages"
|
|
84
|
+
|
|
85
|
+
# Convert to underscores for Ruby conventions (route name, helpers)
|
|
86
|
+
slug = page_name.to_s.underscore
|
|
87
|
+
|
|
88
|
+
# URL path uses hyphens (Rails convention)
|
|
89
|
+
path_value = custom_path || slug.dasherize
|
|
90
|
+
|
|
91
|
+
# Route name uses underscores for path helpers (e.g., about_path)
|
|
92
|
+
route_name = slug.to_sym
|
|
93
|
+
|
|
94
|
+
# Define single GET route
|
|
95
|
+
# Pass hyphenated slug to match Post.slug format in database
|
|
96
|
+
get path_value,
|
|
97
|
+
to: "#{controller}#show",
|
|
98
|
+
defaults: {page: slug.dasherize},
|
|
99
|
+
as: route_name
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/bunko.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "bunko/version"
|
|
4
|
+
require_relative "bunko/configuration"
|
|
5
|
+
require_relative "bunko/models"
|
|
6
|
+
require_relative "bunko/controllers"
|
|
7
|
+
require_relative "bunko/railtie" if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
module Bunko
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
end
|