preflex 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.
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: []