filepress 0.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d19a795578233e2969fa1438a21c7614afab2650666a2a229338c2f322cccc0
4
- data.tar.gz: cfcfbff322ea8909a384f580b6de6082f4a015808acb6d5e3f03a5bc02d17d54
3
+ metadata.gz: cd4430ee794157675a4edeeb36bf4d09ca137300f1ca4b6dc3f1f1b1dd23c387
4
+ data.tar.gz: 6a164c8673b1cd8d1be639f6ff03955bee8f3306a6e25ad8bc4f0274c6fd93ed
5
5
  SHA512:
6
- metadata.gz: a69c6dbb2a9491da1698d9d97ba37ebb4e3e6caceffa1b0d4e7c21d782c0d5d4111869c075fcddb339dd0df471db291a9f43f197cd11dbb9871042b013b5f14c
7
- data.tar.gz: 9590840f2f26567dca12b04fc929c2273a5f21074b179abdf7fb0f96dc22de5720a9d2bb37c8eede06c516ebb7bfcf7b1c8e0ae7eacc6192b782a5a3c9406a4e
6
+ metadata.gz: b707d65fb21ec3d1621705e63c7d7bd1707fb4804982f4029e93946ddc15ada6b155c2bc434aae8e60a16f2d13990815743b6c6d2ab413d1395949a0287d6a10
7
+ data.tar.gz: 1577478c2cbcbfb39931ef4cd00ad920802646da96ba0cdd2d40548bc8d883519b80abd7a3d5fefaf1fbdaa2b02313a07e51deb400ca3a675d7e1ba9101d62a9
data/README.md CHANGED
@@ -15,11 +15,6 @@ Add it to your Gemfile:
15
15
  ```ruby
16
16
  gem "filepress"
17
17
  ```
18
- Then run:
19
-
20
- ```bash
21
- bundle install
22
- ```
23
18
 
24
19
  2. Create a model to represent your content
25
20
 
@@ -35,80 +30,78 @@ Use the `filepress` method in your model class:
35
30
 
36
31
  ```ruby
37
32
  class Post < ApplicationRecord
38
- filepress
33
+ filepress
39
34
  end
40
35
  ```
41
36
 
42
37
  4. Add some content
43
38
 
44
- Create Markdown files in `app/content/posts`. Each file should include frontmatter:
39
+ Create Markdown files in `app/content/posts`. The filename becomes the slug, and YAML frontmatter maps to your model's attributes:
45
40
 
46
41
  ```markdown
47
42
  ---
48
43
  title: My First Post
49
- slug: first
44
+ published: true
50
45
  ---
51
46
 
52
- # Hello, this is my first post
47
+ Hello, this is my first post!
53
48
  ```
54
49
 
55
- 5. Sync your content
56
-
57
- Run the sync task to import your files into the database:
58
-
59
- ```bash
60
- bin/rails filepress:sync
61
- ```
50
+ That's it! Filepress syncs your content to the database automatically — on boot, on deploy, and whenever files change in development. No rake tasks, no deploy hooks.
62
51
 
63
- That’s it! Youre now free to query your content via ActiveRecord like any other model. Filepress doesn't dictate how you render the body—use a Markdown parser like Kramdown, Redcarpet, or similar.
52
+ You're free to query your content via ActiveRecord like any other model. Filepress doesn't dictate how you render the body use a Markdown parser like Kramdown, Redcarpet, or similar.
64
53
 
65
54
  ## How it works
66
55
 
67
56
  Filepress reads your content files, extracts YAML frontmatter, and uses the values to populate or update model attributes. The rest of the file becomes the value of the body attribute (or another field, if configured).
68
57
 
58
+ - Syncs are wrapped in a transaction — if any file fails, nothing changes
59
+ - Unknown frontmatter keys (that don't match a column) are silently ignored
60
+ - In development, Rails' built-in file watcher picks up live edits without a server restart
61
+
69
62
  ## Configuration
70
63
 
71
64
  You can customize how Filepress behaves by passing options to `filepress`:
72
65
 
73
- ### `from:` - Set a custom content directory
66
+ ### `from:` Set a custom content directory
74
67
 
75
68
  ```ruby
76
- class Post
77
- filepress from: "app/my_custom_content_folder"
69
+ class Post < ApplicationRecord
70
+ filepress from: "app/my_custom_content_folder"
78
71
  end
79
72
  ```
80
73
 
81
- ### `glob:` - Use filetypes besides Markdown
74
+ ### `extensions:` Use filetypes besides Markdown
82
75
 
83
76
  ```ruby
84
- class Post
85
- filepress glob: "*.html"
77
+ class Post < ApplicationRecord
78
+ filepress extensions: ["html", "txt"]
86
79
  end
87
80
  ```
88
81
 
89
- ### `key:` - Use a unique identifier other than `slug`
82
+ ### `key:` Use a unique identifier other than `slug`
90
83
 
91
84
  ```ruby
92
- class Post
93
- filepress key: :name
85
+ class Post < ApplicationRecord
86
+ filepress key: :name
94
87
  end
95
88
  ```
96
89
 
97
- ### `body:` - Set which attribute stores the main content
90
+ ### `body:` Set which attribute stores the main content
98
91
 
99
92
  ```ruby
100
- class Post
101
- filepress body: :content
93
+ class Post < ApplicationRecord
94
+ filepress body: :content
102
95
  end
103
96
  ```
104
97
 
105
- ### `destroy_stale:` - Prevent deletion of records when files are removed
98
+ ### `destroy_stale:` Prevent deletion of records when files are removed
106
99
 
107
- By default, Filepress deletes records when the corresponding file is removed. Disable this behavior with:
100
+ By default, Filepress deletes records when the corresponding file is removed. Disable this behaviour with:
108
101
 
109
102
  ```ruby
110
- class Post
111
- filepress destroy_stale: false
103
+ class Post < ApplicationRecord
104
+ filepress destroy_stale: false
112
105
  end
113
106
  ```
114
107
 
@@ -119,4 +112,3 @@ Filepress is for you if:
119
112
  - You prefer writing content in files
120
113
  - You want the convenience of version control for content
121
114
  - And you still want powerful querying, associations, validations and all the other Rails goodness
122
-
@@ -0,0 +1,22 @@
1
+ module Filepress
2
+ class Engine < ::Rails::Engine
3
+ initializer "filepress.model" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ extend Filepress::Model
6
+ end
7
+ end
8
+
9
+ initializer "filepress.file_watcher" do |app|
10
+ content_path = app.root.join("app", "content")
11
+
12
+ app.config.after_initialize do
13
+ if content_path.exist?
14
+ extensions = Filepress.watched_extensions
15
+ app.reloaders << app.config.file_watcher.new([], { content_path.to_s => extensions }) do
16
+ Filepress.sync
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ require "yaml"
2
+
3
+ module Filepress
4
+ class FileParser
5
+ FRONTMATTER_PATTERN = /\A---\s*\n(.*?\n?)---\s*\n?(.*)\z/m
6
+
7
+ attr_reader :attributes, :body
8
+
9
+ def initialize(raw_content)
10
+ @attributes, @body = parse(raw_content)
11
+ end
12
+
13
+ private
14
+
15
+ def parse(raw_content)
16
+ if (match = raw_content.match(FRONTMATTER_PATTERN))
17
+ attributes = YAML.safe_load(match[1], permitted_classes: [Date, Time, Symbol]) || {}
18
+ body = match[2].strip
19
+ else
20
+ attributes = {}
21
+ body = raw_content.strip
22
+ end
23
+
24
+ [attributes.symbolize_keys, body]
25
+ end
26
+ end
27
+ end
@@ -1,15 +1,17 @@
1
1
  module Filepress
2
2
  module Model
3
- def filepress(from: nil, glob: "*.md", key: :slug, body: :body, destroy_stale: true)
4
- class_attribute :filepress_options, instance_accessor: false, default: {}
3
+ def filepress(from: nil, extensions: ["md"], key: :slug, body: :body, destroy_stale: true)
4
+ class_attribute :filepress_options, instance_accessor: false
5
5
 
6
6
  self.filepress_options = {
7
- from: from || "app/content/#{name.underscore.pluralize}",
8
- glob: glob,
7
+ from: from || Rails.root.join("app", "content", model_name.plural).to_s,
8
+ extensions: extensions,
9
9
  key: key,
10
10
  body: body,
11
11
  destroy_stale: destroy_stale
12
12
  }
13
+
14
+ Filepress.register(self, filepress_options)
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,58 @@
1
+ module Filepress
2
+ class Sync
3
+ def initialize(config)
4
+ @model_class = config[:model_class]
5
+ @from = config[:from]
6
+ @extensions = config[:extensions]
7
+ @key = config[:key]
8
+ @body = config[:body]
9
+ @destroy_stale = config[:destroy_stale]
10
+ end
11
+
12
+ def perform
13
+ return unless table_exists?
14
+ return unless File.directory?(@from)
15
+
16
+ synced_identifiers = []
17
+
18
+ @model_class.transaction do
19
+ content_files.each do |file_path|
20
+ identifier_value = File.basename(file_path, ".*")
21
+ parsed = FileParser.new(File.read(file_path))
22
+
23
+ record = @model_class.find_or_initialize_by(@key => identifier_value)
24
+ record.assign_attributes(permitted_attributes(record, parsed.attributes))
25
+ record.public_send(:"#{@body}=", parsed.body)
26
+ record.save!
27
+
28
+ synced_identifiers << identifier_value
29
+ end
30
+
31
+ if @destroy_stale
32
+ @model_class.where.not(@key => synced_identifiers).destroy_all
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def table_exists?
40
+ @model_class.connection_pool.with_connection do
41
+ @model_class.table_exists?
42
+ end
43
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
44
+ false
45
+ end
46
+
47
+ def permitted_attributes(record, attributes)
48
+ known_columns = record.class.column_names.map(&:to_sym)
49
+ attributes.select { |key, _| known_columns.include?(key) }
50
+ end
51
+
52
+ def content_files
53
+ @extensions.flat_map do |ext|
54
+ Dir[File.join(@from, "*.#{ext}")]
55
+ end.sort
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module Filepress
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/filepress.rb CHANGED
@@ -1,39 +1,29 @@
1
1
  require "filepress/version"
2
- require "filepress/railtie"
2
+ require "filepress/engine"
3
+ require "filepress/file_parser"
3
4
  require "filepress/model"
5
+ require "filepress/sync"
4
6
 
5
7
  module Filepress
6
8
  class << self
7
- def sync
8
- Rails.application.eager_load!
9
- models = ActiveRecord::Base.descendants.select { |model| model.respond_to? :filepress_options }
10
-
11
- models.each do |model|
12
- options = model.filepress_options
13
- raise "Filepress options missing for #{model.name}" unless options
14
-
15
- path = Rails.root.join(options[:from])
16
- key = options[:key]
17
- body_attribute = options[:body]
18
-
19
- keys = Dir.glob("#{path}/#{options[:glob]}").map do |file|
20
- raw = File.read(file)
21
- frontmatter, body = raw.split(/^---\s*$/, 3).reject(&:empty?)
22
- data = YAML.safe_load(frontmatter, symbolize_names: true)
23
- content = body.strip
24
-
25
- identifier = data[key]
26
- raise "Missing key `#{key}` in frontmatter for #{file}" unless identifier
9
+ def registry
10
+ @registry ||= {}
11
+ end
27
12
 
28
- record = model.find_or_initialize_by(key => identifier)
29
- record.assign_attributes(data)
30
- record.send("#{body_attribute}=", content)
31
- record.save!
13
+ def register(model_class, options)
14
+ config = options.merge(model_class: model_class)
15
+ registry[model_class.name] = config
16
+ Sync.new(config).perform
17
+ end
32
18
 
33
- identifier
34
- end
19
+ def watched_extensions
20
+ exts = registry.values.flat_map { |config| config[:extensions] }.uniq
21
+ exts.empty? ? ["md"] : exts
22
+ end
35
23
 
36
- model.where.not(key => keys).destroy_all if options[:destroy_stale]
24
+ def sync
25
+ registry.each_value do |config|
26
+ Sync.new(config).perform
37
27
  end
38
28
  end
39
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filepress
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Dawson
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '5.2'
18
+ version: '7.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '5.2'
25
+ version: '7.0'
26
26
  description: Filepress lets you manage content as simple flat files, with all the
27
27
  power of ActiveRecord.
28
28
  email:
@@ -35,10 +35,11 @@ files:
35
35
  - README.md
36
36
  - Rakefile
37
37
  - lib/filepress.rb
38
+ - lib/filepress/engine.rb
39
+ - lib/filepress/file_parser.rb
38
40
  - lib/filepress/model.rb
39
- - lib/filepress/railtie.rb
41
+ - lib/filepress/sync.rb
40
42
  - lib/filepress/version.rb
41
- - lib/tasks/filepress_tasks.rake
42
43
  homepage: https://github.com/carldaws/filepress
43
44
  licenses:
44
45
  - MIT
@@ -53,14 +54,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
54
  requirements:
54
55
  - - ">="
55
56
  - !ruby/object:Gem::Version
56
- version: '0'
57
+ version: '3.1'
57
58
  required_rubygems_version: !ruby/object:Gem::Requirement
58
59
  requirements:
59
60
  - - ">="
60
61
  - !ruby/object:Gem::Version
61
62
  version: '0'
62
63
  requirements: []
63
- rubygems_version: 3.6.7
64
+ rubygems_version: 4.0.3
64
65
  specification_version: 4
65
66
  summary: Write content as flat files, use it anywhere in your Rails app.
66
67
  test_files: []
@@ -1,11 +0,0 @@
1
- module Filepress
2
- class Railtie < ::Rails::Railtie
3
- ActiveSupport.on_load(:active_record) do
4
- extend Filepress::Model
5
- end
6
-
7
- rake_tasks do
8
- Dir[File.expand_path("../tasks/**/*.rake", __dir__)].each { |f| load f }
9
- end
10
- end
11
- end
@@ -1,6 +0,0 @@
1
- namespace :filepress do
2
- desc "Synchronise Filepress models"
3
- task sync: :environment do
4
- Filepress.sync
5
- end
6
- end