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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/CHANGELOG.md +41 -0
  4. data/CLAUDE.md +351 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +641 -0
  7. data/ROADMAP.md +519 -0
  8. data/Rakefile +10 -0
  9. data/lib/bunko/configuration.rb +180 -0
  10. data/lib/bunko/controllers/acts_as.rb +22 -0
  11. data/lib/bunko/controllers/collection.rb +160 -0
  12. data/lib/bunko/controllers.rb +5 -0
  13. data/lib/bunko/models/acts_as.rb +24 -0
  14. data/lib/bunko/models/post_methods/publishable.rb +51 -0
  15. data/lib/bunko/models/post_methods/sluggable.rb +47 -0
  16. data/lib/bunko/models/post_methods/word_countable.rb +76 -0
  17. data/lib/bunko/models/post_methods.rb +75 -0
  18. data/lib/bunko/models/post_type_methods.rb +18 -0
  19. data/lib/bunko/models.rb +6 -0
  20. data/lib/bunko/railtie.rb +22 -0
  21. data/lib/bunko/routing/mapper_methods.rb +103 -0
  22. data/lib/bunko/routing.rb +4 -0
  23. data/lib/bunko/version.rb +5 -0
  24. data/lib/bunko.rb +11 -0
  25. data/lib/tasks/bunko/add.rake +259 -0
  26. data/lib/tasks/bunko/helpers.rb +25 -0
  27. data/lib/tasks/bunko/install.rake +125 -0
  28. data/lib/tasks/bunko/sample_data.rake +128 -0
  29. data/lib/tasks/bunko/setup.rake +186 -0
  30. data/lib/tasks/support/sample_data_generator.rb +399 -0
  31. data/lib/tasks/templates/INSTALL.md +62 -0
  32. data/lib/tasks/templates/config/initializers/bunko.rb.tt +45 -0
  33. data/lib/tasks/templates/controllers/controller.rb.tt +25 -0
  34. data/lib/tasks/templates/controllers/pages_controller.rb.tt +29 -0
  35. data/lib/tasks/templates/db/migrate/create_post_types.rb.tt +14 -0
  36. data/lib/tasks/templates/db/migrate/create_posts.rb.tt +31 -0
  37. data/lib/tasks/templates/models/post.rb.tt +8 -0
  38. data/lib/tasks/templates/models/post_type.rb.tt +8 -0
  39. data/lib/tasks/templates/views/collections/index.html.erb.tt +67 -0
  40. data/lib/tasks/templates/views/collections/show.html.erb.tt +39 -0
  41. data/lib/tasks/templates/views/layouts/bunko_footer.html.erb.tt +3 -0
  42. data/lib/tasks/templates/views/layouts/bunko_nav.html.erb.tt +9 -0
  43. data/lib/tasks/templates/views/layouts/bunko_styles.html.erb.tt +3 -0
  44. data/lib/tasks/templates/views/pages/show.html.erb.tt +16 -0
  45. data/sig/bunko.rbs +4 -0
  46. metadata +116 -0
data/ROADMAP.md ADDED
@@ -0,0 +1,519 @@
1
+ # Bunko 1.0 Roadmap
2
+
3
+ **Goal:** Ship a production-ready CMS gem where a Rails developer can add `gem "bunko"`, run a couple generators, and have a working blog in under 5 minutes. Officially we are only targeting support for Rails but we are trying to keep dependencies as light as possible.
4
+
5
+ **Note:** Version 0.1.0 was released as a placeholder to register the gem name. We're now building toward 1.0.0, using 0.x versions during active development.
6
+
7
+ ---
8
+
9
+ ## Development Status
10
+
11
+ - [x] **Milestone 1: Post Model Behavior** - ✅ COMPLETED
12
+ - [x] **Milestone 2: Collection Controllers** - ✅ COMPLETED
13
+ - [x] **Milestone 3: Installation Generator** - ✅ COMPLETED
14
+ - [x] **Milestone 4: Routing Helpers** - ✅ COMPLETED
15
+ - [x] **Milestone 5: Post Convenience Methods** - ✅ COMPLETED
16
+ - [x] **Milestone 6: Static Pages** - ✅ COMPLETED
17
+ - [ ] **Milestone 7: Configuration** - 🚧 PENDING (core system exists, may need expansion)
18
+ - [ ] **Milestone 8: Documentation** - 🚧 PENDING
19
+ - [ ] **Milestone 9: Release** - 🚧 PENDING
20
+
21
+ ---
22
+
23
+ ## Success Criteria
24
+
25
+ By 1.0, a Rails developer should be able to:
26
+
27
+ 1. ✅ Install Bunko and generate a working blog in < 5 minutes
28
+ 2. ✅ Add a second content collection (e.g., `/docs`) in < 2 minutes
29
+ 3. ✅ Attach the text editor of their choice to create and edit posts
30
+ 4. ✅ Schedule posts for future publication
31
+ 5. ✅ Organize content into different post types without migrations
32
+ 6. ✅ Customize views with their own HTML/CSS
33
+ 7. ✅ Automatically use slug-based URLs instead of IDs
34
+ 8. ✅ Create standalone pages (About, Contact, etc.) without full collections
35
+
36
+ ---
37
+
38
+ ## Milestone 1: Post Model Behavior
39
+
40
+ **Spec:** A Post model with Bunko enabled should have all essential CMS functionality.
41
+
42
+ ### Required Behavior
43
+
44
+ **Scopes & Queries:**
45
+ - Developer can query `Post.published` and only see published posts with `published_at <= Time.current`
46
+ - Developer can query `Post.draft` and only see draft posts
47
+ - Developer can query `Post.scheduled` and only see posts scheduled for future publication
48
+ - Developer can filter posts by type: `Post.by_post_type('blog')` or similar API
49
+ - Default ordering shows most recent posts first
50
+
51
+ **Slug Generation:**
52
+ - When a post is created without a slug, one is auto-generated from the title
53
+ - Slugs are URL-safe (e.g., "Hello World" becomes "hello-world")
54
+ - Slugs are unique within their post_type
55
+ - Developer can provide custom slug and it won't be overwritten
56
+
57
+ **Publishing Workflow:**
58
+ - Post status can be: 'draft', 'published', or 'scheduled'
59
+ - When status changes to 'published' and `published_at` is blank, it auto-sets to current time
60
+ - Posts with status='published' but `published_at` in future are treated as scheduled
61
+ - Invalid status values are rejected
62
+
63
+ **Reading Metrics:**
64
+ - If post has `word_count`, developer can get estimated reading time
65
+ - Reading time calculation is configurable (default ~250 words/minute)
66
+
67
+ **Routing Support:**
68
+ - Users should be able to route #index/#show collections of posts with a simple routes entry, eg something like "mount_bunko :case_studies" should automatically show all posts where post_type = 'case_study' (or 'case_studies'?) in the subfolder /case-studies/ with the slug for that post. Similar to if they wrote this:
69
+ - resources :case_studies, controller: 'case_studies', path: 'case-studies', param: :slug, only: %i[index show]
70
+ - Users should possibly be able to route their core Posts model behind their existing admin / auth area, similar to how they mount sidekiq. This section is intended to be admin only for post editing - so full CRUD but not publicly visible. This should be optional, eg if they want to use a tool like Avo with their [Rhino editor](https://docs.avohq.io/3.0/fields/rhino.html) or [Markdown editor](https://docs.avohq.io/3.0/fields/markdown.html), they don't need to mount this section at all. https://avohq.io/
71
+
72
+ ```
73
+ require "bunko/editor" # require the web UI
74
+
75
+ Rails.application.routes.draw do
76
+ mount Bunko::Editor => "/posts" # access it at http://localhost:3000/posts
77
+ ...
78
+ end
79
+ ```
80
+
81
+ ### Acceptance Test
82
+
83
+ ```ruby
84
+ # Developer can do this:
85
+ class Post < ApplicationRecord
86
+ acts_as_bunko_post
87
+ end
88
+
89
+ # And get this behavior:
90
+ post = Post.create!(title: "Hello World", content: "...", post_type: 'blog')
91
+ post.slug # => "hello-world"
92
+ post.to_param # => "hello-world"
93
+ post.status # => "draft"
94
+
95
+ post.update!(status: 'published')
96
+ post.published_at # => Time.current (auto-set)
97
+
98
+ Post.published.count # => 1
99
+ Post.draft.count # => 0
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Milestone 2: Collection Controllers
105
+
106
+ **Spec:** A controller should be able to serve a content collection with minimal code.
107
+
108
+ ### Required Behavior
109
+
110
+ **Defaults**
111
+ - If user just adds a routes, all of our standard behaviors should be observed.
112
+ - If user chooses to generate their own controller, we should allow that.
113
+ - If user wants to use our controllers but adjust settings through bunko.rb initializer, we should allow that.
114
+
115
+ **Index Action:**
116
+ - Shows all published posts for a given post_type
117
+ - Does not leak other configured posts types in any way or scopes
118
+ - Paginates results - recommend pagy but this should be adaptable? Or perhaps enable Pagy or kamanari behavior in a bunko.rb initializer
119
+ - Allows customization of per_page, ordering, layout
120
+ - Provides access to collection name in views
121
+
122
+ **Show Action:**
123
+ - Finds post by slug
124
+ - Scoped to the correct - eg you should be able to have same slug on 2 different post types
125
+ - Returns 404 if not found (and routes doesn't take over with a 301 or something)
126
+ - Provides access to the post in views
127
+
128
+ **Multiple Post Types:**
129
+ - Single controller can serve multiple related post types
130
+ - Example: Resources controller serves guides, templates, checklists
131
+
132
+ ### Acceptance Test
133
+
134
+ ```ruby
135
+ # Developer can do this:
136
+ class BlogController < ApplicationController
137
+ bunko_collection :blog # or whatever the API is
138
+ end
139
+
140
+ # And get these routes working:
141
+ GET /blog # => BlogController#index (lists blog posts)
142
+ GET /blog/:slug # => BlogController#show (shows single post)
143
+
144
+ # Views have access to:
145
+ @posts # in index
146
+ @post # in show
147
+ @collection_name # 'blog'
148
+
149
+ # Developer can customize:
150
+ class ChangelogController < ApplicationController
151
+ bunko_collection :changelog, per_page: 20, layout: 'docs'
152
+ end
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Milestone 3: Installation Generator
158
+
159
+ **Spec:** Running `rails bunko:install` should create everything needed for a working blog.
160
+
161
+ **Implementation Note:** This milestone was implemented as a two-phase pattern:
162
+ 1. `rails bunko:install` - Creates migrations, models, and initializer
163
+ 2. `rails bunko:setup` - Generates controllers, views, and routes based on configuration
164
+
165
+ This approach allows users to customize their post types in the initializer before generating the controllers/views, and makes it easy to add new collections later.
166
+
167
+ ### Required Behavior
168
+
169
+ **Migration Creation:**
170
+ - Detects database type (PostgreSQL, SQLite, MySQL) and generates appropriate migration
171
+ - Creates `posts` table with essential fields:
172
+ - `title` (string, required)
173
+ - `slug` (string, required, indexed)
174
+ - `content` (text by default, json/jsonb with `--json-content` flag for JSON-based editors)
175
+ - `post_type` (references, required)
176
+ - `status` (string, indexed, default: 'draft') # should this be references to a post_status table?
177
+ - `published_at` (datetime, indexed)
178
+ - Timestamps for created_at and updated_at
179
+ - Creates `post_types` table with essential fields:
180
+ - `name` (string, required)
181
+ - `slug` (string, required, indexed)
182
+ - Timestamps
183
+ - Adds unique constraint on `[slug, post_type]`
184
+ - A post should always have only one post_type, or allow nil? Never multiple though. By default we should load in Post? Or allow nil?
185
+ - Optional fields based on flags (see Generator Options below)
186
+ - User should be able to override our migration and add something like "acts_as_post" to the model to get the same behaviors.
187
+
188
+ **Model Generation:**
189
+ - Creates `app/models/post.rb` with Bunko enabled
190
+ - Creates `app/models/post_type.rb` with Bunko enabled
191
+ - Includes comments explaining customization
192
+
193
+ **Controller Generation:**
194
+ - Creates `app/controllers/blog_controller.rb`
195
+ - Configured to serve 'blog' post type
196
+ - Includes comments explaining how to add more collections
197
+
198
+ **View Generation:**
199
+ - Creates `app/views/blog/index.html.erb` with semantic HTML
200
+ - Creates `app/views/blog/show.html.erb` with semantic HTML
201
+ - No CSS, no JavaScript - just clean HTML with helpful comments
202
+ - Shows title, content, published date, reading time
203
+
204
+ **Route Generation:**
205
+ - Adds routes for blog (index + show)
206
+ - Uses slug as param, not ID
207
+
208
+ **Initializer Generation:**
209
+ - Creates `config/initializers/bunko.rb`
210
+ - Includes common configuration options (commented out with examples)
211
+
212
+ **Post-Install Message:**
213
+ - Shows next steps (run migration, create first post)
214
+ - Links to documentation
215
+
216
+ ### Generator Options
217
+
218
+ - `--skip-seo` - Skip adding SEO fields (title_tag, meta_description)
219
+ - `--json-content` - Use json/jsonb for content field instead of text (for JSON-based editors)
220
+
221
+ ### Acceptance Test
222
+
223
+ ```bash
224
+ # Developer runs:
225
+ $ rails bunko:install
226
+ $ rails db:migrate
227
+
228
+ # Result: They can visit /blog and see a working (empty) blog
229
+ # They can create a post in console and see it at /blog/post-slug
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Milestone 4: Routing Helpers
235
+
236
+ **Spec:** Setting up routes for collections should be simple and conventional.
237
+
238
+ ### Required Behavior
239
+
240
+ **Route DSL:**
241
+ - Developer can call `bunko_collection :blog` instead of writing full resources line
242
+ - Supports custom paths (e.g., `bunko_collection :case_study, path: 'case-studies'`)
243
+ - Supports limiting actions (e.g., `only: [:index]` for index-only collection) # not critical
244
+ - Supports custom controller names
245
+
246
+ ### Acceptance Test
247
+
248
+ ```ruby
249
+ # Developer can do this in config/routes.rb:
250
+ Rails.application.routes.draw do
251
+ bunko_collection :blog
252
+ bunko_collection :docs
253
+ bunko_collection :case_study, path: 'case-studies' #perhaps path should be automatically hyphenated and/or inflected/pluralized?
254
+ end
255
+
256
+ # And get these routes:
257
+ # /blog => blog#index
258
+ # /blog/:slug => blog#show
259
+ # /docs => docs#index
260
+ # /docs/:slug => docs#show
261
+ # /case-studies => case_study#index
262
+ # /case-studies/:slug => case_study#show
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Milestone 5: Post Convenience Methods
268
+
269
+ **Spec:** Common CMS view patterns should be available as Post instance methods for clean, conflict-free usage in views.
270
+
271
+ **Implementation Note:** Originally planned as view helpers, we decided to implement these as Post model methods instead. This approach:
272
+ - Avoids namespace conflicts (no `bunko_` prefix needed for generic helper names)
273
+ - Keeps views cleaner (`post.excerpt` vs `bunko_excerpt(post)`)
274
+ - Works identically in index loops and show views
275
+
276
+ ### Required Behavior
277
+
278
+ **Content Formatting:**
279
+ - `post.excerpt(length: 160, omission: "...")` - returns truncated content, strips HTML, preserves word boundaries
280
+ - `post.reading_time_text` - returns "X min read" string (extends existing `reading_time` integer method)
281
+
282
+ **Date Formatting:**
283
+ - `post.published_date(format = :long)` - returns formatted published_at using I18n.l
284
+ - Supports Rails date formats: `:long`, `:short`, `:db`, custom strftime
285
+
286
+ **Navigation:**
287
+ - Not needed - routing DSL automatically generates helpers like `blog_path`, `blog_post_path(post)`
288
+
289
+ ### Acceptance Test
290
+
291
+ ```erb
292
+ <!-- Index view: loop over posts -->
293
+ <% @posts.each do |post| %>
294
+ <article>
295
+ <h2><%= link_to post.title, blog_post_path(post) %></h2>
296
+ <p class="meta">
297
+ <%= post.published_date %> · <%= post.reading_time_text %>
298
+ </p>
299
+ <p><%= post.excerpt %></p>
300
+ </article>
301
+ <% end %>
302
+
303
+ <!-- Show view: single post -->
304
+ <article>
305
+ <h1><%= @post.title %></h1>
306
+ <p class="meta">
307
+ <%= @post.published_date(:long) %> · <%= @post.reading_time_text %>
308
+ </p>
309
+ <div class="content">
310
+ <%= @post.content %>
311
+ </div>
312
+ </article>
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Milestone 6: Static Pages
318
+
319
+ **Spec:** Developers should be able to create standalone pages (About, Contact, Privacy Policy) without needing a full collection with an index page.
320
+
321
+ ### Required Behavior
322
+
323
+ **Routing DSL:**
324
+ - `bunko_page :about` creates a single route: `GET /about → pages#show`
325
+ - Supports custom paths: `bunko_page :about, path: "about-us"`
326
+ - Supports custom controllers: `bunko_page :contact, controller: "static_pages"`
327
+ - Works with namespaces: `namespace :legal do bunko_page :privacy end`
328
+
329
+ **PagesController:**
330
+ - Single shared controller for all pages (no per-page controller generation)
331
+ - Smart view resolution: checks for custom page template (e.g., `pages/about.html.erb`) first
332
+ - Falls back to default `pages/show.html.erb` if custom template doesn't exist
333
+ - Raises 404 if page Post not found
334
+
335
+ **Configuration:**
336
+ - Opt-out support: `config.allow_static_pages = false`
337
+ - Reserved "pages" post_type namespace with validation error
338
+ - Auto-generated during `rails bunko:setup` if enabled (default: true)
339
+
340
+ **Architecture:**
341
+ - Uses same Post model as collections (maintains one-model architecture)
342
+ - Pages stored with `post_type = "pages"`
343
+ - Slug must match route name (e.g., route `bunko_page :about` expects Post with slug "about")
344
+
345
+ ### Acceptance Test
346
+
347
+ ```ruby
348
+ # In config/routes.rb:
349
+ Rails.application.routes.draw do
350
+ bunko_page :about
351
+ bunko_page :contact
352
+ bunko_page :privacy
353
+ end
354
+
355
+ # Create page content:
356
+ pages_type = PostType.find_by(name: "pages")
357
+
358
+ Post.create!(
359
+ title: "About Us",
360
+ content: "<p>Welcome to our company...</p>",
361
+ post_type: pages_type,
362
+ slug: "about", # Must match route name
363
+ status: "published"
364
+ )
365
+
366
+ # Result:
367
+ # GET /about → renders pages/about.html.erb or pages/show.html.erb
368
+ # GET /contact → 404 (no Post with slug "contact" exists yet)
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Milestone 7: Configuration
374
+
375
+ **Spec:** Bunko behavior should be customizable via initializer without modifying gem code.
376
+
377
+ ### Required Behavior
378
+
379
+ **Configurable Options:**
380
+ - Post model name (default: 'Post')
381
+ - Valid post types (default: ['post'])
382
+ - Valid statuses (default: ['draft', 'published', 'scheduled'])
383
+ - Default status (default: 'draft')
384
+ - Reading speed in words/minute (default: 250)
385
+ - Excerpt length (default: 160)
386
+ - Slug generation strategy (default: parameterize)
387
+
388
+ **Configuration API:**
389
+ - Developer uses block syntax in initializer
390
+ - Configuration is globally accessible
391
+ - Invalid configuration values are validated
392
+
393
+ ### Acceptance Test
394
+
395
+ ```ruby
396
+ # Developer can configure in config/initializers/bunko.rb:
397
+ Bunko.configure do |config|
398
+ config.post_type "post"
399
+ config.post_type "page"
400
+ config.post_type "doc"
401
+ config.post_type "tutorial"
402
+
403
+ config.reading_speed = 200
404
+ config.excerpt_length = 200
405
+ config.slug_generator = ->(title) { title.parameterize.truncate(50) }
406
+ end
407
+
408
+ # And it affects behavior:
409
+ post = Post.create!(title: "Very Long Title...")
410
+ # slug uses custom generator
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Milestone 8: Documentation
416
+
417
+ **Spec:** Documentation should be excellent, examples should be practical.
418
+
419
+ ### Required Deliverables
420
+
421
+ **README.md:**
422
+ - Philosophy and goals clearly stated
423
+ - Installation instructions (add to Gemfile, run generator)
424
+ - Quick start guide (5 minute blog)
425
+ - Multi-collection setup example
426
+ - Configuration options documented
427
+ - Customization patterns explained
428
+ - What Bunko doesn't do (auth, admin UI, etc.)
429
+
430
+ **EXAMPLES.md:**
431
+ - Basic blog setup
432
+ - Blog + docs setup
433
+ - Custom fields using metadata
434
+ - Custom scopes
435
+ - Overriding views
436
+ - Integration with admin gems (Avo, Administrate, etc.)
437
+
438
+ **Code Documentation:**
439
+ - All public APIs have clear documentation
440
+ - Generated code includes helpful comments
441
+ - Configuration options explained in generated initializer
442
+
443
+ **Example Apps:**
444
+ - `examples/basic_blog` - Minimal blog
445
+ - `examples/multi_collection` - Blog + docs + changelog
446
+
447
+ ### Acceptance Test
448
+
449
+ New developer can:
450
+ - Read README and understand what Bunko does in < 2 minutes
451
+ - Follow quick start and have working blog in < 5 minutes
452
+ - Find answer to "how do I customize X?" in documentation
453
+ - Clone an example app and see Bunko in action
454
+
455
+ ---
456
+
457
+ ## Milestone 9: Release
458
+
459
+ **Spec:** 1.0.0 is published to RubyGems and ready for production use.
460
+
461
+ ### Required Before Release
462
+
463
+ **Compatibility:**
464
+ - Works with Rails 8.0+ and follows Rails EOL maintenance policy
465
+ - Works with Ruby 3.2, 3.3, 3.4 and follows Ruby EOL maintenance policy
466
+ - Works with PostgreSQL, SQLite, MySQL
467
+ - Test coverage > 90%
468
+ - All Standard linter checks pass
469
+
470
+ **Package:**
471
+ - bunko.gemspec has no TODOs
472
+ - Proper description and summary
473
+ - Correct homepage and source URLs
474
+ - Appropriate version number (1.0.0)
475
+ - CHANGELOG.md updated
476
+
477
+ **Documentation:**
478
+ - README complete and accurate
479
+ - EXAMPLES.md or docs have practical examples
480
+ - Generated code has helpful comments
481
+ - GitHub release notes written
482
+
483
+ **Distribution:**
484
+ - Gem builds successfully
485
+ - Gem published to RubyGems
486
+ - GitHub release created
487
+ - Installation tested in fresh Rails app
488
+
489
+ ### Acceptance Test
490
+
491
+ ```bash
492
+ # Any developer can:
493
+ $ gem install bunko
494
+ $ rails new myblog
495
+ $ cd myblog
496
+ $ bundle add bunko
497
+ $ rails bunko:install
498
+ $ rails db:migrate
499
+ $ rails bunko:setup
500
+ $ rails bunko:sample_data
501
+ $ rails server
502
+
503
+ # Visit http://localhost:3000/blog and see working blog with sample content
504
+ ```
505
+
506
+ ---
507
+
508
+ ## Out of Scope for 1.0
509
+
510
+ These are excellent features but not required for initial release:
511
+
512
+ - **Admin UI generator** - `rails generate bunko:admin`
513
+ - **Seed task** - `rails bunko:seed` for sample content
514
+ - **Custom fields DSL** - Beyond metadata jsonb
515
+ - **Publishing callbacks** - `after_publish`, etc.
516
+ - **Versioning support** - Draft history, rollback
517
+ - **Multi-collection controllers** - Single controller, many types
518
+ - **Author associations** - belongs_to :author
519
+ - **Category/tag models** - For now, use strings or metadata
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bunko
4
+ class Configuration
5
+ class PostTypeCustomizer
6
+ def initialize(post_type_hash)
7
+ @post_type_hash = post_type_hash
8
+ end
9
+
10
+ def title=(value)
11
+ @post_type_hash[:title] = value
12
+ end
13
+
14
+ # Future: Add more customization methods
15
+ # def per_page=(value)
16
+ # @post_type_hash[:per_page] = value
17
+ # end
18
+ end
19
+
20
+ class CollectionCustomizer
21
+ def initialize(collection_hash)
22
+ @collection_hash = collection_hash
23
+ end
24
+
25
+ def title=(value)
26
+ @collection_hash[:title] = value
27
+ end
28
+
29
+ def post_types=(value)
30
+ # Normalize post_types to array of names (using underscores, not hyphens)
31
+ @collection_hash[:post_types] = Array(value).map { |pt| pt.to_s.parameterize.tr("-", "_") }
32
+ end
33
+
34
+ def scope=(callable)
35
+ @collection_hash[:scope] = callable
36
+ end
37
+
38
+ # Future: Add more customization methods
39
+ # def per_page=(value)
40
+ # @collection_hash[:per_page] = value
41
+ # end
42
+ end
43
+
44
+ attr_accessor :reading_speed, :excerpt_length, :auto_update_word_count, :valid_statuses, :post_types, :collections, :allow_static_pages
45
+
46
+ def initialize
47
+ @reading_speed = 250 # words per minute
48
+ @excerpt_length = 160 # characters
49
+ @auto_update_word_count = true # automatically update word_count when content changes
50
+ @valid_statuses = %w[draft published scheduled]
51
+ @post_types = [] # Must be configured in initializer
52
+ @collections = [] # Multi-type collections
53
+ @allow_static_pages = true # Enable standalone pages feature by default
54
+ end
55
+
56
+ def post_type(name, title: nil, &block)
57
+ # Validate name format (must use underscores, not hyphens, for Ruby class naming)
58
+ name_str = name.to_s
59
+
60
+ if name_str.include?("-")
61
+ raise ArgumentError, "PostType name '#{name_str}' cannot contain hyphens. Use underscores instead (e.g., 'case_study'). URLs will automatically use hyphens (/case-study/)."
62
+ end
63
+
64
+ unless name_str.match?(/\A[a-z0-9_]+\z/)
65
+ raise ArgumentError, "PostType name '#{name_str}' must contain only lowercase letters, numbers, and underscores"
66
+ end
67
+
68
+ # Reserved name for static pages feature
69
+ if name_str == "pages"
70
+ raise ArgumentError, "PostType name 'pages' is reserved for the static pages feature. Use config.allow_static_pages to control this feature."
71
+ end
72
+
73
+ # Check for conflicts with existing collections
74
+ if collection_exists?(name_str)
75
+ raise ArgumentError, "PostType name '#{name_str}' conflicts with existing collection name"
76
+ end
77
+
78
+ # Auto-generate title from name (e.g., "case_study" → "Case Study")
79
+ generated_title = title || name_str.titleize
80
+
81
+ post_type = {name: name_str, title: generated_title}
82
+
83
+ # Allow customization via block (block overrides params)
84
+ if block_given?
85
+ customizer = PostTypeCustomizer.new(post_type)
86
+ block.call(customizer)
87
+ end
88
+
89
+ @post_types << post_type
90
+ end
91
+
92
+ def collection(name, title: nil, post_types: nil, scope: nil, &block)
93
+ # Validate name format (must use underscores, not hyphens)
94
+ name_str = name.to_s
95
+
96
+ if name_str.include?("-")
97
+ raise ArgumentError, "Collection name '#{name_str}' cannot contain hyphens. Use underscores instead (e.g., 'long_reads'). URLs will automatically use hyphens (/long-reads/)."
98
+ end
99
+
100
+ unless name_str.match?(/\A[a-z0-9_]+\z/)
101
+ raise ArgumentError, "Collection name '#{name_str}' must contain only lowercase letters, numbers, and underscores"
102
+ end
103
+
104
+ # Check for conflicts with existing post_types
105
+ if post_type_exists?(name_str)
106
+ raise ArgumentError, "Collection name '#{name_str}' conflicts with existing PostType name"
107
+ end
108
+
109
+ # Check for conflicts with existing collections
110
+ if collection_exists?(name_str)
111
+ raise ArgumentError, "Collection '#{name_str}' already exists"
112
+ end
113
+
114
+ # Require at least post_types param or block
115
+ unless post_types || block_given?
116
+ raise ArgumentError, "Collection '#{name_str}' requires either post_types parameter or a configuration block"
117
+ end
118
+
119
+ # Auto-generate title from name (e.g., "long_reads" → "Long Reads")
120
+ generated_title = title || name_str.titleize
121
+
122
+ # Normalize post_types to array of names (using underscores, not hyphens)
123
+ normalized_post_types = post_types ? Array(post_types).map { |pt| pt.to_s.parameterize.tr("-", "_") } : []
124
+
125
+ collection = {
126
+ name: name_str,
127
+ title: generated_title,
128
+ post_types: normalized_post_types,
129
+ scope: scope
130
+ }
131
+
132
+ # Allow customization via block (block overrides params)
133
+ if block_given?
134
+ customizer = CollectionCustomizer.new(collection)
135
+ block.call(customizer)
136
+ end
137
+
138
+ # Validate that post_types was set
139
+ if collection[:post_types].empty?
140
+ raise ArgumentError, "Collection '#{name_str}' must specify at least one post_type"
141
+ end
142
+
143
+ @collections << collection
144
+ end
145
+
146
+ def find_post_type(name)
147
+ @post_types.find { |pt| pt[:name] == name.to_s }
148
+ end
149
+
150
+ def find_collection(name)
151
+ @collections.find { |c| c[:name] == name.to_s }
152
+ end
153
+
154
+ private
155
+
156
+ def post_type_exists?(name)
157
+ @post_types.any? { |pt| pt[:name] == name.to_s }
158
+ end
159
+
160
+ def collection_exists?(name)
161
+ @collections.any? { |c| c[:name] == name.to_s }
162
+ end
163
+ end
164
+
165
+ class << self
166
+ attr_writer :configuration
167
+
168
+ def configuration
169
+ @configuration ||= Configuration.new
170
+ end
171
+
172
+ def configure
173
+ yield(configuration)
174
+ end
175
+
176
+ def reset_configuration!
177
+ @configuration = Configuration.new
178
+ end
179
+ end
180
+ end