dynamic_fields_rails 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 37e9e3ca85c3be34eed3f654ccf6862116e12051606e9dcfbf876d5d666633d8
4
+ data.tar.gz: 41d9125df6eef2233a53983f824a1b945312f918036cfa873eb357e67b73b5c3
5
+ SHA512:
6
+ metadata.gz: 53e0ed3d0cf4de042b4a4dc9fecf7911d04f4d13c44cc897cd7f05d387bfde7127ba8cc56ff9fa85ee7f0863bc59090de93404f6eb8d122257f454ba8cf5fb8b
7
+ data.tar.gz: 62e776b9dcad1d03000c9832ee8200afb0a08deb0f4dd9196518ad3effa196ef97b54a0f8d0d48f39d42ba09f74cdff3c5b6900faa2914a02e6b9eaed54752c3
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Vincent Rolea
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ [![Gem Version](https://badge.fury.io/rb/dynamic_fields_rails.svg)](https://badge.fury.io/rb/dynamic_fields_rails)
2
+
3
+ [![Tests](https://github.com/virolea/dynamic_fields/actions/workflows/tests.yml/badge.svg)](https://github.com/virolea/dynamic_fields/actions/workflows/tests.yml)
4
+
5
+ # DynamicFields
6
+
7
+ DynamicFields allows to add persisted attributes to ActiveRecord models without the need for a migration.
8
+
9
+ You might want to add a temporary attribute, for monitoring or reporting purposes, or avoid to pollute your domain tables with marketing-related fields.
10
+
11
+ With DynamicFields, this is as easy as the following:
12
+
13
+ ```ruby
14
+ class User
15
+ include DynamicFields::ActiveRecord
16
+
17
+ has_dynamic_field :subscribe_to_newsletter, field_type: :boolean
18
+ end
19
+
20
+ user = User.create(subscribe_to_newsletter: true)
21
+ user.subscribe_to_newsletter # => true
22
+ ```
23
+
24
+ The declared attribute works seamlessly like a traditionnal ActiveRecord attribute. This means you can use features such as validations without any futher work:
25
+
26
+ ```ruby
27
+ class Post
28
+ include DynamicFields::ActiveRecord
29
+
30
+ has_dynamic_field :title
31
+
32
+ validates :title, presence: true
33
+ end
34
+
35
+ post = Post.new
36
+ post.valid? # => false
37
+ post.errors.full_messages # => ["Title can't be blank"]
38
+ ```
39
+
40
+ ## Installation
41
+ Add this line to your application's Gemfile:
42
+
43
+ ```ruby
44
+ gem "dynamic_fields"
45
+ ```
46
+
47
+ And then execute:
48
+ ```bash
49
+ $ bundle
50
+ ```
51
+
52
+ Or install it yourself as:
53
+ ```bash
54
+ $ gem install dynamic_fields
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Persisted attributes are backed by the `dynamic_fields_attributes` table. After installing the gem run migrations to add the required tables:
60
+
61
+ ```
62
+ rails db:migrate
63
+ ```
64
+
65
+ Once the migrations are done, start adding attributes to model by including the `DynamicFields::ActiveRecord` module any active record model:
66
+
67
+ ```ruby
68
+ class Post
69
+ include DynamicFields::ActiveRecord
70
+ end
71
+ ```
72
+
73
+ And voilà! Your model is ready to declare new dynamic fields. The API is simple:
74
+
75
+ ```ruby
76
+ class Post
77
+ include DynamicFields::ActiveRecord
78
+
79
+ # Use `has_dynamic_field` with the field name to declare a new attribute.
80
+ has_dynamic_field :title
81
+
82
+ # Default type for a field is :string. Use the `field_type` option to declare another field type
83
+ has_dynamic_field :reading_time, field_type: :integer
84
+ has_dynamic_field :published, field_type: :boolean
85
+ end
86
+ ```
87
+
88
+ For now, DynamicFields allow for three field types;
89
+ - String (default)
90
+ - Integer
91
+ - Boolean
92
+
93
+ The list will likely grow with time, usage and needs.
94
+
95
+ ## License
96
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,5 @@
1
+ module DynamicFields
2
+ class Attribute::BooleanAttribute < Attribute
3
+ attribute :value, :boolean
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module DynamicFields
2
+ class Attribute::IntegerAttribute < Attribute
3
+ attribute :value, :integer
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module DynamicFields
2
+ class Attribute::StringAttribute < Attribute
3
+ end
4
+ end
@@ -0,0 +1,29 @@
1
+ module DynamicFields
2
+ class Attribute < ApplicationRecord
3
+ DEFAULT_FIELD_TYPE = :string
4
+
5
+ AVAILABLE_ATTRIBUTE_TYPES = %i[
6
+ string
7
+ boolean
8
+ integer
9
+ ].freeze
10
+
11
+ belongs_to :record, polymorphic: true
12
+
13
+ validates :name, presence: true
14
+ validates :name, uniqueness: { scope: [:record_type, :record_id] }
15
+
16
+ class << self
17
+ def attribute_class_for_attribute_type(attribute_type)
18
+ case attribute_type
19
+ when :string
20
+ return "DynamicFields::Attribute::StringAttribute"
21
+ when :integer
22
+ return "DynamicFields::Attribute::IntegerAttribute"
23
+ when :boolean
24
+ return "DynamicFields::Attribute::BooleanAttribute"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,12 @@
1
+ class CreateDynamicFieldsTables < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :dynamic_fields_attributes do |t|
4
+ t.references :record, polymorphic: true, null: false
5
+ t.string :name
6
+ t.string :type
7
+ t.string :value
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ module DynamicFields
2
+ module ActiveRecord
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :dynamic_fields_attributes, class_name: "DynamicFields::Attribute", as: :record
7
+ end
8
+
9
+ class_methods do
10
+ def has_dynamic_field(field_name, field_type: DynamicFields::Attribute::DEFAULT_FIELD_TYPE)
11
+ raise ArgumentError, "#{field_type} is not a valid DynamicFields attribute type. Available values include #{DynamicFields::Attribute::AVAILABLE_ATTRIBUTE_TYPES}" unless DynamicFields::Attribute::AVAILABLE_ATTRIBUTE_TYPES.include?(field_type.to_sym)
12
+
13
+ has_one :"#{field_name}_attribute", -> { where(name: field_name) }, class_name: DynamicFields::Attribute.attribute_class_for_attribute_type(field_type), as: :record, inverse_of: :record, dependent: :destroy
14
+
15
+ define_method(field_name) do
16
+ if dynamic_field_changes["#{field_name}"]
17
+ dynamic_field_changes["#{field_name}"].value
18
+ else
19
+ dynamic_fields_attributes.find { |attribute| attribute.name == field_name.to_s }&.value
20
+ end
21
+ end
22
+
23
+ define_method("#{field_name}=") do |value|
24
+ # Not checking for `value.blank?` here to account for boolean fields, as false.blank? is true
25
+ dynamic_field_changes["#{field_name}"] = if value == "" || value.nil?
26
+ DynamicFields::Changes::Delete.new(field_name, self)
27
+ else
28
+ DynamicFields::Changes::CreateOrUpdate.new(value, field_name, field_type, self)
29
+ end
30
+ end
31
+
32
+ after_save { dynamic_field_changes[field_name.to_s]&.save }
33
+ end
34
+
35
+ end
36
+
37
+ def dynamic_field_changes
38
+ @dynamic_field_changes ||= {}
39
+ end
40
+
41
+ def reload(*)
42
+ super.tap { @dynamic_field_changes = nil }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module DynamicFields
2
+ class Changes::CreateOrUpdate
3
+ attr_reader :record, :value, :field_name, :field_type, :attribute
4
+
5
+ def initialize(value, field_name, field_type, record)
6
+ @value, @field_name, @field_type, @record = value, field_name, field_type, record
7
+ @attribute = find_or_build_attribute
8
+ end
9
+
10
+ def save
11
+ attribute.value = value
12
+ record.public_send("#{field_name}_attribute=", attribute)
13
+ end
14
+
15
+ private
16
+
17
+ def find_or_build_attribute
18
+ record.dynamic_fields_attributes.find_or_initialize_by(
19
+ name: field_name,
20
+ type: DynamicFields::Attribute.attribute_class_for_attribute_type(field_type)
21
+ )
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module DynamicFields
2
+ class Changes::Delete
3
+ attr_reader :record, :field_name, :value
4
+
5
+ def initialize(field_name, record)
6
+ @field_name, @record = field_name, record
7
+ @value = nil
8
+ end
9
+
10
+ def save
11
+ record.public_send("#{field_name}_attribute=", nil)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module DynamicFields
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace DynamicFields
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module DynamicFields
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ require "dynamic_fields/version"
2
+ require "dynamic_fields/engine"
3
+
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.new
7
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
8
+ loader.push_dir(File.expand_path(".", __dir__))
9
+ loader.setup
10
+
11
+ module DynamicFields
12
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :dynamic_fields do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamic_fields_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Vincent Rolea
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.1.3.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.1.3.4
27
+ description: Add persisted fields to active record models without migrations.
28
+ email:
29
+ - 3525369+virolea@users.noreply.github.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/models/dynamic_fields/attribute.rb
38
+ - app/models/dynamic_fields/attribute/boolean_attribute.rb
39
+ - app/models/dynamic_fields/attribute/integer_attribute.rb
40
+ - app/models/dynamic_fields/attribute/string_attribute.rb
41
+ - config/routes.rb
42
+ - db/migrate/20240729120435_create_dynamic_fields_tables.rb
43
+ - lib/dynamic_fields.rb
44
+ - lib/dynamic_fields/active_record.rb
45
+ - lib/dynamic_fields/changes/create_or_update.rb
46
+ - lib/dynamic_fields/changes/delete.rb
47
+ - lib/dynamic_fields/engine.rb
48
+ - lib/dynamic_fields/version.rb
49
+ - lib/tasks/dynamic_fields_tasks.rake
50
+ homepage: https://github.com/virolea/dynamic_fields
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/virolea/dynamic_fields
55
+ source_code_uri: https://github.com/virolea/dynamic_fields
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.5.3
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Add persisted fields to active record models without migrations.
75
+ test_files: []