madmin-static_models 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 50348135ce76b0e9c74e3fc26c2d0cf99af7a371ccfa1cfc0d5aa1e8b25d6f84
4
+ data.tar.gz: 7a45e2cf576dbf07bbf285c749cd0af4c57ce186090f80f6b17e8ed22f8dd3e8
5
+ SHA512:
6
+ metadata.gz: c7ac57164d43bb5d708d788b8838ccbce6dea8a3b0e0379df90869b620700a1fb2a0b7072e099849f570427abe1d50fdb16aa931eb1c7ccb508bb87a32922ddb
7
+ data.tar.gz: '072779c79e1ce89a000ddfbf7b5e86bb07f957f3da64bb5af81f1f8a1c7cd6063548d9047583e5f1c44ea26b6fdaac273b231c26dc16c70c44c1b0ce7153454c'
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release: ActiveHash adapter (covers ActiveHash, ActiveYaml, ActiveJson and ActiveFile models), read-only resources, in-memory search/sort/pagination, generator support, and an adapter API for other static backends.
6
+ - Requires madmin >= 2.6 (extension load hooks and seams).
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Madmin Static Models
2
+
3
+ Browse read-only, in-memory models in your [Madmin](https://github.com/excid3/madmin) admin.
4
+
5
+ Supports [active_hash](https://github.com/active-hash/active_hash) models out of the box — `ActiveHash::Base`, `ActiveYaml::Base`, `ActiveJson::Base` and `ActiveFile::Base` — with an adapter layer for adding other static backends (e.g. frozen_record) later.
6
+
7
+ Static resources get:
8
+
9
+ - Index, show, search, sorting and pagination — all done in memory, no SQL
10
+ - Automatic read-only behavior: no new/edit/delete links, write actions redirect away
11
+ - Generator support: `rails g madmin:install` and `rails g madmin:resource` pick up static models
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "madmin-static_models"
19
+ ```
20
+
21
+ Requires madmin >= 2.6.
22
+
23
+ ## Usage
24
+
25
+ Define a static model:
26
+
27
+ ```ruby
28
+ class Country < ActiveHash::Base
29
+ fields :name, :code
30
+
31
+ self.data = [
32
+ {id: 1, name: "United States", code: "US"},
33
+ {id: 2, name: "Canada", code: "CA"}
34
+ ]
35
+ end
36
+ ```
37
+
38
+ Generate its admin resource (or write it by hand):
39
+
40
+ ```bash
41
+ rails g madmin:resource Country
42
+ ```
43
+
44
+ ```ruby
45
+ class CountryResource < Madmin::Resource
46
+ attribute :id, form: false
47
+ attribute :name
48
+ attribute :code
49
+ end
50
+ ```
51
+
52
+ That's it — the resource shows up in Madmin like any other, minus the write actions.
53
+
54
+ ## How it works
55
+
56
+ The gem attaches to Madmin through its `ActiveSupport` load hooks (`:madmin_resource`, `:madmin_resource_controller`) and prepends small modules that answer differently for static models and call `super` for everything else:
57
+
58
+ - `Resource.readonly?` returns true, which makes Madmin hide write links and block write actions
59
+ - `Resource.model_column_names` comes from the adapter instead of the database
60
+ - Pagination, sorting and search run over plain arrays in memory
61
+ - `show_path`/`edit_path` are built manually since static records don't support polymorphic routing
62
+
63
+ ## Adding a backend
64
+
65
+ An adapter is a class with three methods. Register it and matching models are treated as static:
66
+
67
+ ```ruby
68
+ class FrozenRecordAdapter < Madmin::StaticModels::Adapter
69
+ def self.handles?(model)
70
+ model < ::FrozenRecord::Base
71
+ end
72
+
73
+ def self.column_names(model)
74
+ [model.primary_key.to_s, *model.attributes].uniq
75
+ end
76
+
77
+ def self.models
78
+ ::FrozenRecord::Base.descendants
79
+ end
80
+ end
81
+
82
+ Madmin::StaticModels.register(FrozenRecordAdapter)
83
+ ```
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ bundle install
89
+ bundle exec rake test
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,25 @@
1
+ module Madmin
2
+ module StaticModels
3
+ # Base class for static model backends. An adapter declares which model
4
+ # classes it manages and how to introspect them. Register one with:
5
+ #
6
+ # Madmin::StaticModels.register(MyAdapter)
7
+ class Adapter
8
+ # True if this adapter manages the given model class.
9
+ def self.handles?(model)
10
+ false
11
+ end
12
+
13
+ # Ordered attribute names (strings) for the model, primary key first.
14
+ def self.column_names(model)
15
+ []
16
+ end
17
+
18
+ # All model classes managed by this adapter that are currently loaded.
19
+ # Used by the install generator to create resources.
20
+ def self.models
21
+ []
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module Madmin
2
+ module StaticModels
3
+ module Adapters
4
+ # Backend for active_hash models. Covers ActiveHash::Base and its
5
+ # subclasses, which includes ActiveYaml::Base, ActiveJSON::Base and
6
+ # ActiveFile::Base.
7
+ class ActiveHash < Adapter
8
+ # Abstract base classes shipped by the active_hash gem itself.
9
+ BASE_CLASSES = %w[ActiveHash::Base ActiveFile::Base ActiveYaml::Base ActiveJson::Base ActiveJSON::Base]
10
+
11
+ def self.handles?(model)
12
+ model < ::ActiveHash::Base
13
+ end
14
+
15
+ def self.column_names(model)
16
+ ([model.primary_key.to_s] + model.field_names.map(&:to_s)).uniq
17
+ end
18
+
19
+ def self.models
20
+ ObjectSpace.each_object(::ActiveHash::Base.singleton_class).reject do |model|
21
+ model.name.nil? || BASE_CLASSES.include?(model.name)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ module Madmin
2
+ module StaticModels
3
+ # Prepended to Madmin::ResourceController.
4
+ module ControllerExtension
5
+ private
6
+
7
+ def scoped_resources
8
+ return super unless StaticModels.static_model?(resource.model)
9
+
10
+ resources = resource.model.send(valid_scope)
11
+ resources = Madmin::Search.new(resources, resource, search_term).run
12
+
13
+ return resources if sort_column.blank?
14
+
15
+ sort_static(resources, sort_column, sort_direction)
16
+ end
17
+
18
+ def paginate_collection(collection)
19
+ return super unless StaticModels.static_model?(resource.model)
20
+
21
+ paginate_static(collection)
22
+ end
23
+
24
+ def paginate_static(collection)
25
+ records = collection.to_a
26
+
27
+ # Pagy >= 43 paginates plain arrays natively, with the request context
28
+ # that its nav helpers need.
29
+ return pagy(records) if defined?(Pagy::Method) && is_a?(Pagy::Method)
30
+
31
+ # Older pagy: build the pager by hand.
32
+ page = [params[:page].to_i, 1].max
33
+ defaults = Pagy::DEFAULT || {}
34
+ limit = (params[:limit] || defaults[:limit] || defaults[:items] || 20).to_i
35
+ pager = begin
36
+ Pagy.new(count: records.size, page: page, limit: limit)
37
+ rescue ArgumentError
38
+ Pagy.new(count: records.size, page: page, items: limit)
39
+ end
40
+ per_page = pager.respond_to?(:limit) ? pager.limit : pager.items
41
+ [pager, records.slice(pager.offset, per_page) || []]
42
+ end
43
+
44
+ def sort_static(collection, column, direction)
45
+ records = collection.to_a.sort do |a, b|
46
+ a_value = a.public_send(column) if a.respond_to?(column)
47
+ b_value = b.public_send(column) if b.respond_to?(column)
48
+ if a_value.nil? || b_value.nil?
49
+ # Sort records without a value last
50
+ (a_value.nil? ? 1 : 0) <=> (b_value.nil? ? 1 : 0)
51
+ else
52
+ (a_value <=> b_value) || (a_value.to_s <=> b_value.to_s)
53
+ end
54
+ end
55
+ (direction.to_s == "desc") ? records.reverse : records
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ require "rails/generators"
2
+ require "generators/madmin/install/install_generator"
3
+ require "generators/madmin/resource/resource_generator"
4
+
5
+ module Madmin
6
+ module StaticModels
7
+ module InstallGeneratorExtension
8
+ def generate_resources
9
+ generateable_models.each do |model|
10
+ if StaticModels.static_model?(model) || model.table_exists?
11
+ call_generator "madmin:resource", model.to_s
12
+ else
13
+ puts "Skipping #{model} because database table does not exist"
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def generateable_models
21
+ static = StaticModels.adapters.flat_map(&:models).reject { |model| model.name.nil? }
22
+ super.to_a + static
23
+ end
24
+ end
25
+
26
+ module ResourceGeneratorExtension
27
+ private
28
+
29
+ def options_for_attribute(name)
30
+ adapter = StaticModels.adapter_for(model)
31
+ return super unless adapter
32
+
33
+ if name == model.primary_key.to_s
34
+ {form: false}
35
+ elsif !adapter.column_names(model).include?(name)
36
+ {index: false}
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ Madmin::Generators::InstallGenerator.prepend Madmin::StaticModels::InstallGeneratorExtension
44
+ Madmin::Generators::ResourceGenerator.prepend Madmin::StaticModels::ResourceGeneratorExtension
@@ -0,0 +1,34 @@
1
+ module Madmin
2
+ module StaticModels
3
+ class Railtie < ::Rails::Railtie
4
+ initializer "madmin.static_models" do |app|
5
+ # Madmin::ResourceController is reloadable, so patch it lazily via
6
+ # madmin's load hook whenever it (re)loads.
7
+ ActiveSupport.on_load(:madmin_resource_controller) do
8
+ prepend Madmin::StaticModels::ControllerExtension
9
+ end
10
+
11
+ ActiveSupport.on_load(:madmin_resource) do
12
+ singleton_class.prepend Madmin::StaticModels::ResourceExtension
13
+ end
14
+
15
+ # Plain lib classes; referencing them triggers madmin's autoload.
16
+ Madmin::Search.prepend Madmin::StaticModels::SearchExtension
17
+ Madmin::ResourceBuilder.prepend Madmin::StaticModels::ResourceBuilderExtension
18
+
19
+ app.config.after_initialize do
20
+ unless Madmin::Resource.singleton_class.include?(Madmin::StaticModels::ResourceExtension)
21
+ # The :madmin_resource load hook never fired, so this madmin
22
+ # predates the extension seams (readonly?, model_column_names,
23
+ # paginate_collection) that this gem builds on.
24
+ warn "madmin-static_models requires madmin >= 2.6 (extension load hooks and seams)."
25
+ end
26
+ end
27
+ end
28
+
29
+ generators do
30
+ require "madmin/static_models/generator_extensions"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module Madmin
2
+ module StaticModels
3
+ # Prepended to Madmin::ResourceBuilder.
4
+ module ResourceBuilderExtension
5
+ def attributes
6
+ adapter = StaticModels.adapter_for(model)
7
+ adapter ? adapter.column_names(model) : super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ module Madmin
2
+ module StaticModels
3
+ # Prepended to Madmin::Resource's singleton class.
4
+ module ResourceExtension
5
+ def readonly?
6
+ StaticModels.static_model?(model) || super
7
+ end
8
+
9
+ def model_column_names
10
+ adapter = StaticModels.adapter_for(model)
11
+ adapter ? adapter.column_names(model) : super
12
+ end
13
+
14
+ # Static records can't be passed to polymorphic_path (no #becomes,
15
+ # not backed by ActiveModel routing), so build the paths by hand.
16
+ def show_path(record)
17
+ return super unless StaticModels.static_model?(model)
18
+
19
+ "#{index_path}/#{record.id}"
20
+ end
21
+
22
+ def edit_path(record)
23
+ return super unless StaticModels.static_model?(model)
24
+
25
+ "#{index_path}/#{record.id}/edit"
26
+ end
27
+
28
+ def infer_type(name)
29
+ return super unless StaticModels.static_model?(model)
30
+
31
+ if model_column_names.include?(name.to_s)
32
+ :string
33
+ elsif model.respond_to?(:reflect_on_association) && (association = model.reflect_on_association(name))
34
+ type_for_association(association)
35
+ else
36
+ :string
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ module Madmin
2
+ module StaticModels
3
+ # Prepended to Madmin::Search. Static models have no SQL backend, so
4
+ # search filters records in memory instead.
5
+ module SearchExtension
6
+ def run
7
+ return super unless StaticModels.static_model?(@resource.model)
8
+ return @scoped_resource.all if query.blank?
9
+
10
+ static_search(@scoped_resource)
11
+ end
12
+
13
+ private
14
+
15
+ def static_search(resources)
16
+ pattern = Regexp.new(Regexp.escape(query), Regexp::IGNORECASE)
17
+ fields = search_attributes.flat_map { |attribute| searchable_fields(attribute) }
18
+ matched = resources.all.select do |record|
19
+ fields.any? { |field| record.respond_to?(field) && record.public_send(field).to_s.match?(pattern) }
20
+ end
21
+ resources.respond_to?(:where) ? resources.where(id: matched.map(&:id)) : matched
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module Madmin
2
+ module StaticModels
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,39 @@
1
+ require "madmin"
2
+ require "active_hash"
3
+
4
+ require "madmin/static_models/version"
5
+ require "madmin/static_models/adapter"
6
+ require "madmin/static_models/adapters/active_hash"
7
+ require "madmin/static_models/resource_extension"
8
+ require "madmin/static_models/resource_builder_extension"
9
+ require "madmin/static_models/controller_extension"
10
+ require "madmin/static_models/search_extension"
11
+ require "madmin/static_models/railtie"
12
+
13
+ module Madmin
14
+ module StaticModels
15
+ class << self
16
+ def adapters
17
+ @adapters ||= []
18
+ end
19
+
20
+ def register(adapter)
21
+ adapters << adapter unless adapters.include?(adapter)
22
+ end
23
+
24
+ # Returns the adapter that manages this model class, or nil for
25
+ # regular (ActiveRecord) models.
26
+ def adapter_for(model)
27
+ return nil unless model.is_a?(Class)
28
+
29
+ adapters.find { |adapter| adapter.handles?(model) }
30
+ end
31
+
32
+ def static_model?(model)
33
+ !adapter_for(model).nil?
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Madmin::StaticModels.register(Madmin::StaticModels::Adapters::ActiveHash)
@@ -0,0 +1 @@
1
+ require "madmin/static_models"
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: madmin-static_models
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Anthony Veaudry
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: madmin
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: active_hash
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ description: Browse read-only, in-memory models like ActiveHash and ActiveYaml in
41
+ your Madmin admin, with an adapter layer for adding other static backends.
42
+ email:
43
+ - anthony@veaudry.pro
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - README.md
50
+ - lib/madmin-static_models.rb
51
+ - lib/madmin/static_models.rb
52
+ - lib/madmin/static_models/adapter.rb
53
+ - lib/madmin/static_models/adapters/active_hash.rb
54
+ - lib/madmin/static_models/controller_extension.rb
55
+ - lib/madmin/static_models/generator_extensions.rb
56
+ - lib/madmin/static_models/railtie.rb
57
+ - lib/madmin/static_models/resource_builder_extension.rb
58
+ - lib/madmin/static_models/resource_extension.rb
59
+ - lib/madmin/static_models/search_extension.rb
60
+ - lib/madmin/static_models/version.rb
61
+ homepage: https://github.com/anthony0030/madmin-static_models
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.2.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 4.0.16
80
+ specification_version: 4
81
+ summary: Static model support (ActiveHash, ActiveYaml, ...) for Madmin
82
+ test_files: []