paulie_lombardo 1.0.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: fb56753c7d4d30bcaa0d23a1e7ee369f5f5e4d9eca7c273a6b0f19075826894b
4
+ data.tar.gz: 31773ebb1fc6ce4596166bebdef54ea91f1fd13a56f947493d19853355dda7cf
5
+ SHA512:
6
+ metadata.gz: fa1f3c6fbb13b13de3d86febd773a4e7cda4c761930ea414c43d4e6eb5a786e4936ecc7000d71e721cb31a8ea633b282c120b7ec8465b1e49e796050641313bb
7
+ data.tar.gz: 6b96d4d26951d97abb621a8d0f514ca7273c63423f0324a30a6f4715f536ae4b9c03d9d5f3506b7ff90cd5e9ca1914231dc65284810a305270a545e1a50af6fe
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record"
4
+
5
+ module PaulieLombardo
6
+ module Generators
7
+ # Creating the migration file for the paulie_notes table.
8
+ class ActiveRecordGenerator < Rails::Generators::Base
9
+ # Provides ActiveRecord implementation of #next_migration_number for #migration_template
10
+ include ::ActiveRecord::Generators::Migration
11
+
12
+ argument :tables, type: :array, default: []
13
+
14
+ source_root File.join(File.dirname(__FILE__), "templates")
15
+
16
+ def validate_tables_exist
17
+ return if tables.empty?
18
+
19
+ connection = ActiveRecord::Base.connection
20
+
21
+ tables.each do |table_name|
22
+ name = table_name.tableize
23
+
24
+ unless connection.table_exists?(name)
25
+ msg = set_color("Error: Table '#{name}' does not exist in the database.", :red, :bold)
26
+ raise Thor::Error, msg
27
+ end
28
+ end
29
+ end
30
+
31
+ def create_migration_file
32
+ migration_template "create_paulie_notes_table.rb.erb", "db/migrate/create_paulie_notes.rb"
33
+ end
34
+
35
+ def add_counter_cache_fields_to_tables
36
+ return if tables.empty?
37
+
38
+ migration_template "add_paulie_notes_count_to_tables.rb.erb", "db/migrate/add_paulie_notes_count.rb"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ module Generators
5
+ # Primary setup generator for installing the gem into a Rails app
6
+ class InstallGenerator < Rails::Generators::Base
7
+ argument :tables, type: :array, default: []
8
+
9
+ # Invokes the configured ORM generator (e.g., ActiveRecord) to create migrations
10
+ hook_for :orm, required: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ module Generators
5
+ # Stub generator for Mongoid ORM implementation
6
+ class MongoidGenerator < Rails::Generators::Base
7
+ def create_migration_file
8
+ say_status :warning, "Mongoid support requires implementation for paulie_lombardo.", :yellow
9
+ say " Currently, automatic document/migration generation for Mongoid is not supported.", :yellow
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ module Generators
5
+ # Stub generator for Sequel ORM implementation
6
+ class SequelGenerator < Rails::Generators::Base
7
+ def create_migration_file
8
+ say_status :warning, "Sequel support requires implementation for paulie_lombardo.", :yellow
9
+ say " Currently, automatic migration generation for Sequel is not supported.", :yellow
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ # The decision-making authority that decides
5
+ # whether a page view request should be approved and logged.
6
+ class Consigliere
7
+ def initialize(dossier)
8
+ @dossier = dossier
9
+ end
10
+
11
+ def approve?
12
+ no_prior_record?
13
+ end
14
+
15
+ private
16
+
17
+ # Checks if there are no previous records for the given visitor.
18
+ #
19
+ # Automatically handles collision/fingerprint overlaps
20
+ # multiple records with the same request_hash can coexist if they belong to different session_hashes
21
+ #
22
+ # @return [Boolean]
23
+ # true if no record exists and a new one should be created
24
+ # false if a record already exists
25
+ def no_prior_record?
26
+ # 1. Highest priority: if user is logged in, check purely by user_id
27
+ return PaulieNote.where(@dossier.user_profile).none? if @dossier.user_id.present?
28
+
29
+ # 2. If session_hash is blank, simply check existence via request_hash
30
+ return PaulieNote.where(@dossier.request_profile).none? if @dossier.session_hash.blank?
31
+
32
+ # 3. When session_hash present - returns false early if a matching record exists
33
+ return false if PaulieNote.where(@dossier.session_profile).exists?
34
+
35
+ # Finds a session-less record matching the request profile and links the current session_hash.
36
+ return false if session_attached?
37
+
38
+ true
39
+ end
40
+
41
+ def session_attached?
42
+ note = PaulieNote.find_by(@dossier.request_profile)
43
+ note&.update(session_hash: @dossier.session_hash)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ # Adds controller methods
5
+ module Controller
6
+ # Creates a new paulie_note record
7
+ def paulie_note(*target)
8
+ PaulieLombardo.bouncer(self, *target) do |target, data|
9
+ target.paulie_notes.create!(data)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ # Build paulie_note attributes
5
+ class Dossier
6
+ attr_reader :data, :user_id, :interval, :target
7
+
8
+ def initialize(controller, target, scope = nil)
9
+ @request = controller.request
10
+ @session = controller.session
11
+ @user_id = user_id_from_warden
12
+ @target = identify_subject(target)
13
+ @interval = configured_interval(scope)
14
+
15
+ build
16
+ end
17
+
18
+ def request_hash
19
+ data[:request_hash]
20
+ end
21
+
22
+ def session_hash
23
+ data[:session_hash]
24
+ end
25
+
26
+ def request_profile
27
+ base_profile(request_hash: request_hash, session_hash: nil)
28
+ end
29
+
30
+ def session_profile
31
+ base_profile(session_hash: session_hash)
32
+ end
33
+
34
+ def user_profile
35
+ base_profile(user_id: user_id)
36
+ end
37
+
38
+ UUID_NAMESPACE = "9cac5761-c7b1-4c75-be24-1d4db88ac468"
39
+ private_constant :UUID_NAMESPACE
40
+
41
+ private
42
+
43
+ attr_reader :request
44
+
45
+ def build
46
+ @data = {
47
+ session_hash: generate_uuid(@session.id),
48
+ request_hash: generate_uuid(raw_string),
49
+ referrer: request.referrer
50
+ }
51
+
52
+ @data[:user_id] = user_id if user_id.present?
53
+ end
54
+
55
+ def generate_uuid(value)
56
+ return if value.blank?
57
+
58
+ Digest::UUID.uuid_v5(UUID_NAMESPACE, value.to_s)
59
+ end
60
+
61
+ def raw_string
62
+ ["request_hash",
63
+ request.user_agent,
64
+ request.accept_language,
65
+ mask_ip,
66
+ http_sec_ch_ua,
67
+ http_sec_ch_ua_platform,
68
+ http_sec_ch_ua_mobile].compact.join("/")
69
+ end
70
+
71
+ def mask_ip
72
+ addr = IPAddr.new(request.remote_ip)
73
+
74
+ if addr.ipv4?
75
+ addr.mask(24).to_s
76
+ else
77
+ addr.mask(48).to_s
78
+ end
79
+ end
80
+
81
+ def http_sec_ch_ua
82
+ request.headers["HTTP_SEC_CH_UA"]
83
+ end
84
+
85
+ def http_sec_ch_ua_platform
86
+ request.headers["HTTP_SEC_CH_UA_PLATFORM"]
87
+ end
88
+
89
+ def http_sec_ch_ua_mobile
90
+ request.headers["HTTP_SEC_CH_UA_MOBILE"]
91
+ end
92
+
93
+ def user_id_from_warden
94
+ warden = request.env["warden"]
95
+ return nil unless warden
96
+
97
+ warden.user(:user)&.id
98
+ end
99
+
100
+ def configured_interval(scope)
101
+ return scope if @target.is_a?(PaulieStaticPage)
102
+ return unless @target.respond_to?(:paulie_notes_options)
103
+
104
+ @target.paulie_notes_options[:interval]
105
+ end
106
+
107
+ def identify_subject(target)
108
+ target.is_a?(ActiveRecord::Base) ? target : PaulieStaticPage.new(target)
109
+ end
110
+
111
+ def base_profile(attributes)
112
+ attributes[:noteable_type] = @target.class.polymorphic_name
113
+ attributes[:noteable_id] = @target.id.to_s
114
+ attributes[:viewed_on] = Time.current.utc.to_date if interval == :daily
115
+ attributes
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ # Adds 'has_paulie_notes' association to ActiveRecord models
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # rubocop:disable-next Naming/PredicatePrefix
10
+ def has_paulie_notes(interval: nil)
11
+ class_attribute :paulie_notes_options, default: {}
12
+ self.paulie_notes_options = { interval: interval }.freeze
13
+
14
+ has_many :paulie_notes, as: :noteable, dependent: :delete_all
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents an individual note record associated polymorphically with any model.
4
+ class PaulieNote < ActiveRecord::Base
5
+ belongs_to :noteable, polymorphic: true, counter_cache: true
6
+
7
+ before_create :set_viewed_on
8
+
9
+ def noteable
10
+ if noteable_type == PaulieStaticPage.polymorphic_name
11
+ PaulieStaticPage.new(noteable_id)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def set_viewed_on
20
+ self.viewed_on ||= Time.current.utc.to_date
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents a non-ActiveRecord target for polymorphic `PaulieNote` associations
4
+ class PaulieStaticPage
5
+ attr_reader :id
6
+
7
+ def initialize(name)
8
+ @id = name.to_s.underscore
9
+ end
10
+
11
+ def self.polymorphic_name
12
+ name
13
+ end
14
+
15
+ # Emulates `has_many :paulie_notes` creation for non-ActiveRecord target
16
+ class AssociationProxy
17
+ def initialize(target)
18
+ @target = target
19
+ end
20
+
21
+ def create!(attributes = {})
22
+ PaulieNote.create!(
23
+ attributes.merge(
24
+ noteable_type: @target.class.polymorphic_name,
25
+ noteable_id: @target.id
26
+ )
27
+ )
28
+ end
29
+ end
30
+
31
+ def paulie_notes
32
+ AssociationProxy.new(self)
33
+ end
34
+
35
+ def paulie_notes_count
36
+ PaulieNote.where(noteable_type: self.class.polymorphic_name, noteable_id: id).count
37
+ end
38
+
39
+ # Stubs to prevent SQL execution and NoMethodError for the counter_cache column
40
+ #
41
+ # rubocop:disable-next Style/SingleLineMethods
42
+ # rubocop:disable-next Naming/PredicatePrefix
43
+ class << self
44
+ def unscoped; self; end
45
+ def where!(*); self; end
46
+ def primary_key; :id; end
47
+ def has_query_constraints?; false; end
48
+ def composite_primary_key?; false; end
49
+ def update_counters(*_args); 0 end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ # Railtie integration to automatically load Controller and Model concerns
5
+ # into Rails framework components upon initialization.
6
+ class Railtie < ::Rails::Railtie
7
+ initializer "paulie_lombardo.controller" do
8
+ ActiveSupport.on_load(:action_controller) do
9
+ include PaulieLombardo::Controller
10
+ end
11
+ end
12
+
13
+ initializer "paulie_lombardo.active_record" do
14
+ ActiveSupport.on_load(:active_record) do
15
+ include PaulieLombardo::Model
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaulieLombardo
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paulie_lombardo/paulie_static_page"
4
+ require "paulie_lombardo/dossier"
5
+ require "paulie_lombardo/controller"
6
+ require "paulie_lombardo/paulie_note"
7
+ require "paulie_lombardo/model"
8
+ require "paulie_lombardo/consigliere"
9
+ require "paulie_lombardo/railtie"
10
+
11
+ # Main entry point for the PaulieLombardo
12
+ module PaulieLombardo
13
+ def self.bouncer(*)
14
+ dossier = Dossier.new(*)
15
+ consigliere = Consigliere.new(dossier)
16
+
17
+ paulie_note = yield(dossier.target, dossier.data) if consigliere.approve?
18
+
19
+ if paulie_note.present?
20
+ paulie_note.noteable
21
+ else
22
+ dossier.target
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paulie_lombardo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sasha Stadnyk
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: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.0.0
26
+ description: |
27
+ Paulie is the silent mobster standing at the entrance
28
+ of your page, closely watching every visitor and logging it to the database"
29
+ email: stadniklksndr@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/generators/paulie_lombardo/active_record/active_record_generator.rb
35
+ - lib/generators/paulie_lombardo/install/install_generator.rb
36
+ - lib/generators/paulie_lombardo/mongoid/mongoid_generator.rb
37
+ - lib/generators/paulie_lombardo/sequel/sequel_generator.rb
38
+ - lib/paulie_lombardo.rb
39
+ - lib/paulie_lombardo/consigliere.rb
40
+ - lib/paulie_lombardo/controller.rb
41
+ - lib/paulie_lombardo/dossier.rb
42
+ - lib/paulie_lombardo/model.rb
43
+ - lib/paulie_lombardo/paulie_note.rb
44
+ - lib/paulie_lombardo/paulie_static_page.rb
45
+ - lib/paulie_lombardo/railtie.rb
46
+ - lib/paulie_lombardo/version.rb
47
+ homepage: https://github.com/stadniklksndr/paulie_lombardo
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ rubygems_mfa_required: 'true'
52
+ source_code_uri: https://github.com/stadniklksndr/paulie_lombardo
53
+ changelog_uri: https://github.com/stadniklksndr/paulie_lombardo/blob/master/CHANGELOG.md
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4.0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.19
69
+ specification_version: 4
70
+ summary: Ruby on Rails page view counter
71
+ test_files: []