preflex 0.2.0

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: b7bfaa76370b12382b4e911acf713b5e29432f0989301039c3b42ed96a2efe4d
4
+ data.tar.gz: 9d76ddac8cb8753d2f86dac792168337fe8e69c2062c087feafde3d16859f0ab
5
+ SHA512:
6
+ metadata.gz: b21919439815819dd2f77c46d36c234a6534d009addfba13a37736a2ea683b92b18d135e6f07c7b739f7d0cbbd0fd8d99559c625ae7a236a7fa9c4d11e94f919
7
+ data.tar.gz: 471c5d55662b128ee78d715eac741f8fb8186cd0d1e95f16da05869e4df1274aa977b00341122b8151d092edd4005534c0d03c4833ef9e139de20bb4d403f7aa
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Owais
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,28 @@
1
+ # Preflex
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "preflex"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install preflex
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ 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("../example/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
1
+ module Preflex::SetCurrentContext
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :preflex_set_current_context
6
+ end
7
+
8
+ protected
9
+
10
+ def preflex_set_current_context
11
+ Preflex::Current.controller_instance = self
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ module Preflex
2
+ class ApplicationController < Preflex.base_controller_class_for_update
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module Preflex
2
+ class PreferencesController < ApplicationController
3
+ # POST /preferences
4
+ # Params:
5
+ # klass
6
+ # name
7
+ # value
8
+ def update
9
+ klass = params[:klass].constantize
10
+ raise "Expected #{params[:klass]} to be a subclass of Preflex::Preference" unless klass < Preflex::Preference
11
+
12
+ klass.set(params[:name], params[:value])
13
+ head :ok
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,78 @@
1
+ module Preflex
2
+ module PreferencesHelper
3
+ extend self
4
+
5
+ # PREFLEX_PREFERENCE_JS = File.read(Preferences::Preflex::Engine.root.join("app", "static", "preflex_preference.js"))
6
+ def script_tag(*preference_klasses)
7
+ return ''.html_safe if preference_klasses.empty?
8
+
9
+ base = <<~JS
10
+ const CSRF_TOKEN = "#{Preflex::Current.controller_instance.send(:form_authenticity_token)}"
11
+ const UPDATE_PATH = "#{Preflex::Engine.routes.url_helpers.preferences_path}"
12
+ class PreflexPreference {
13
+ constructor(klass, data) {
14
+ this.klass = klass
15
+ this.localStorageKey = `PreflexPreference-${klass}`
16
+
17
+ this.data = data
18
+ this.dataLocal = JSON.parse(localStorage.getItem(this.localStorageKey) || '{}')
19
+ }
20
+
21
+ get(name) {
22
+ this.ensurePreferenceExists(name)
23
+
24
+ const fromServer = this.data[name]
25
+ const fromServerUpdatedAt = this.data[`${name}_updated_at_epoch`] || 0
26
+
27
+ const fromLocal = this.dataLocal[name]
28
+ const fromLocalUpdatedAt = this.dataLocal[`${name}_updated_at_epoch`] || 0
29
+
30
+ if(fromLocalUpdatedAt > fromServerUpdatedAt) {
31
+ this.updateOnServer(name, fromLocal)
32
+ return fromLocal
33
+ }
34
+
35
+ return fromServer
36
+ }
37
+
38
+ set(name, value) {
39
+ this.ensurePreferenceExists(name)
40
+
41
+ this.dataLocal[name] = value
42
+ this.dataLocal[`${name}_updated_at_epoch`] = Date.now()
43
+
44
+ localStorage.setItem(this.localStorageKey, JSON.stringify(this.dataLocal))
45
+ this.updateOnServer(name, value)
46
+ document.dispatchEvent(new CustomEvent('preflex:preference-updated', { detail: { name, value } }))
47
+ }
48
+
49
+ updateOnServer(name, value) {
50
+ fetch(UPDATE_PATH, {
51
+ method: 'POST',
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "X-CSRF-TOKEN": CSRF_TOKEN
55
+ },
56
+ body: JSON.stringify({ klass: this.klass, name, value })
57
+ })
58
+ }
59
+
60
+ ensurePreferenceExists(name) {
61
+ if(!this.data.hasOwnProperty(name)) {
62
+ throw new Error(`Preference ${name} was not defined.`)
63
+ }
64
+ }
65
+ }
66
+ JS
67
+
68
+ js = [base]
69
+ js += preference_klasses.map do |klass|
70
+ raise 'Expected #{klass} to be a sub-class of Preflex::Preference' unless klass < Preflex::Preference
71
+
72
+ "window['#{klass.name}'] = new PreflexPreference('#{klass.name}', #{klass.current.data_for_js});"
73
+ end
74
+
75
+ "<script>#{js.join("\n")}</script>".html_safe
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ module Preflex
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Preflex
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :controller_instance
4
+ attribute :preference_cache
5
+ end
6
+ end
@@ -0,0 +1,73 @@
1
+ module Preflex
2
+ class Preference < ApplicationRecord
3
+ self.store_attribute_unset_values_fallback_to_default = true
4
+
5
+ store :data, coder: JSON
6
+
7
+ def get(name)
8
+ name = name.to_sym
9
+ self.class.ensure_preference_exists(name)
10
+
11
+ send(name)
12
+ end
13
+
14
+ def set(name, value)
15
+ name = name.to_sym
16
+ self.class.ensure_preference_exists(name)
17
+
18
+ send("#{name}=", value)
19
+ send("#{name}_updated_at_epoch=", (Time.current.to_f * 1000).round)
20
+ save!
21
+ end
22
+
23
+ def data_for_js
24
+ data.to_json
25
+ end
26
+
27
+ def self.preference(name, type, default: nil, private: false)
28
+ name = name.to_sym
29
+
30
+ @preferences ||= Set.new
31
+ @preferences.add(name)
32
+
33
+ store_attribute(:data, name, type, default: default)
34
+ store_attribute(:data, "#{name}_updated_at_epoch".to_sym, :integer, default: 0)
35
+ end
36
+
37
+ def self.current_owner(controller_instance)
38
+ raise '
39
+ Please define a class method called owner that returns the owner of this preference.
40
+ You can use `controller_instance` to refer to things like current_user/etc.
41
+ You can return either:
42
+ an ActiveRecord object persisted in the DB
43
+ or any object that responds to `id` - returning a unique id
44
+ or a string that uniquely identifies the owner
45
+ Example:
46
+ def self.current_owner(controller_instance)
47
+ controller_instance.current_user
48
+ end
49
+ '
50
+ end
51
+
52
+ def self.for(owner)
53
+ owner = "#{owner.class.name}-#{owner.id}" if owner.respond_to?(:id)
54
+ PreferenceCache.for(self, owner.to_s)
55
+ end
56
+
57
+ def self.current
58
+ self.for(current_owner(Preflex::Current.controller_instance))
59
+ end
60
+
61
+ def self.get(name)
62
+ current.get(name)
63
+ end
64
+
65
+ def self.set(name, value)
66
+ current.set(name, value)
67
+ end
68
+
69
+ def self.ensure_preference_exists(name)
70
+ raise "Preference #{name} was not defined. Make sure you define it (e.g. `preference :#{name}, :integer, default: 10`)" unless @preferences&.include?(name)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ module Preflex
2
+ class PreferenceCache
3
+ def initialize
4
+ @cache = {}
5
+ end
6
+
7
+ def for(klass, owner)
8
+ @cache[klass] ||= {}
9
+ @cache[klass][owner] ||= klass.find_or_initialize_by(owner: owner)
10
+ end
11
+
12
+ def self.for(klass, owner)
13
+ self.current.for(klass, owner)
14
+ end
15
+
16
+ def self.current
17
+ Preflex::Current.preference_cache ||= new
18
+ end
19
+ end
20
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Preflex::Engine.routes.draw do
2
+ post 'preferences' => 'preferences#update', as: :preferences
3
+ end
@@ -0,0 +1,11 @@
1
+ class CreatePreflexPreferences < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :preflex_preferences do |t|
4
+ t.string :type
5
+ t.string :owner, limit: 500
6
+ t.text :data, limit: 16.megabytes - 1
7
+ t.index ['type', 'owner']
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ require 'store_attribute'
2
+
3
+ module Preflex
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Preflex
6
+
7
+ config.to_prepare do
8
+ Preflex.base_controller_class.include(Preflex::SetCurrentContext)
9
+ Preflex.base_controller_class_for_update.include(Preflex::SetCurrentContext) unless Preflex.base_controller_class_for_update < Preflex.base_controller_class
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Preflex
2
+ VERSION = "0.2.0"
3
+ end
data/lib/preflex.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "preflex/version"
2
+ require "preflex/engine"
3
+
4
+ module Preflex
5
+ mattr_accessor :base_controller_class
6
+ mattr_accessor :base_controller_class_for_update
7
+
8
+ def self.base_controller_class
9
+ (@@base_controller_class || '::ApplicationController').constantize
10
+ end
11
+
12
+ def self.base_controller_class_for_update
13
+ @@base_controller_class_for_update&.constantize || self.base_controller_class
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ desc "Configure preflex"
2
+ task :preflex do
3
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../template/install.rb", __dir__)}"
4
+ end
@@ -0,0 +1,47 @@
1
+ say "🗄️ Running migration to create a table to store preferences..."
2
+ rails_command("preflex:install:migrations")
3
+ rails_command("db:migrate")
4
+
5
+ say "🛠️ Setting up routes and a dummy initializer file"
6
+ route "mount Preflex::Engine => '/preflex'"
7
+ initializer 'preflex.rb', <<~RUBY
8
+ # If you have a custom base controller, uncomment the below line and set it here.
9
+ #Preflex.base_controller_class = '::ApplicationController'
10
+
11
+ # If you want to make it so that the controller that handles requests to update preferences(from the client-side) inherits from a different base controller, uncomment the below line and set it here.
12
+ #Preflex.base_controller_class_for_update = '::ApplicationController'
13
+ RUBY
14
+
15
+ create_example_preference = !no?("✨ Do you want to set up an example preference class? (Y/n)")
16
+
17
+ file_name = nil
18
+ if create_example_preference
19
+ name = ask("✨ What do you want to call this class? (E.g UserPreference, FeatureFlag, CustomerSettings, etc.)").presence || 'CustomerSettings'
20
+ file_name = "app/models/#{name.underscore}.rb"
21
+ file file_name, <<~RUBY
22
+ class #{name} < Preflex::Preference
23
+ preference :autoplay, :boolean, default: true
24
+ preference :volume, :integer, default: 75
25
+ preference :title, :string, default: 'Mr.'
26
+ preference :favorite_colors, :json, default: ["red", "blue"]
27
+
28
+ def self.current_owner(controller_instance)
29
+ # You'd want to modify this to return the correct owner (whatever that is for you - can be an account/user/session/customer/etc - whatever you call it)
30
+ controller_instance.current_user
31
+ end
32
+ end
33
+ RUBY
34
+ end
35
+
36
+ say ""
37
+ say ""
38
+ say ""
39
+ say "✅ ✅ ✅ All done ✅ ✅ ✅"
40
+ say ""
41
+ say "You might want to take a look at #{file_name} and update it's `current_owner` method definition." if create_example_preference
42
+ say "" if create_example_preference
43
+ say "You can also change the base controller that preflex uses if warranted. Take a look at config/initializers/preflex.rb"
44
+ say ""
45
+ say "If you'd like to easily read and write preferences from the client side, just add `Preflex::PreferencesHelper.script_tag(*AllThePreferenceClassesYouHave)` (e.g `Preflex::PreferencesHelper.script_tag(UserPreference, CustomerSettings)`) to the head tag in your layout file (e.g app/views/layouts/application.html.erb) and read the docs at https://github.com/owaiswiz/preflex"
46
+
47
+ def run_bundle; end;
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: preflex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Owais
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-12 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: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: store_attribute
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
41
+ description: 'A simple but powerful Rails engine for storing preferences, feature
42
+ flags, etc. With support for reading/writing values client-side! '
43
+ email:
44
+ - owaiswiz@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - app/controllers/concerns/preflex/set_current_context.rb
53
+ - app/controllers/preflex/application_controller.rb
54
+ - app/controllers/preflex/preferences_controller.rb
55
+ - app/helpers/preflex/preferences_helper.rb
56
+ - app/models/preflex/application_record.rb
57
+ - app/models/preflex/current.rb
58
+ - app/models/preflex/preference.rb
59
+ - app/models/preflex/preference_cache.rb
60
+ - config/routes.rb
61
+ - db/migrate/20240211162939_create_preflex_preferences.rb
62
+ - lib/preflex.rb
63
+ - lib/preflex/engine.rb
64
+ - lib/preflex/version.rb
65
+ - lib/tasks/preflex_tasks.rake
66
+ - lib/template/install.rb
67
+ homepage: https://github.com/owaiswiz/preflex
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/owaiswiz/preflex
72
+ source_code_uri: https://github.com/owaiswiz/preflex
73
+ changelog_uri: https://github.com/owaiswiz/preflex/releases
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.4.10
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: A simple but powerful Rails engine for storing preferences, feature flags,
93
+ etc. With support for reading/writing values client-side!
94
+ test_files: []