public_code 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 391563ed134d7ff113b44b5608ddf2068dd18ca6048190aa1913e03a7313adc8
4
- data.tar.gz: 7b1e6be8d4b8bb9abdac10b8aaabf5a269c37b2fe119437f1387562985537538
3
+ metadata.gz: 85226caa9d5bbc68eedc7aecda5105a079c13733853522736aa1fb04f6d99477
4
+ data.tar.gz: 5bfe2a1075d98348dad53b5b569d86e21c16938908f91ef08ea97015076df351
5
5
  SHA512:
6
- metadata.gz: d84156a63e7644dd3df2f9beb2c33b20cc12c2936db07c0f2d30770aad3cfaa2572439e91a0fabb33691fdcbbbaf333b1b09c577ebcf3e0aae7221db28b4a736
7
- data.tar.gz: ea22466225e19e92fe4fbe4d737153bd0c03c0d8ffa26aaac497d2fb38548066157e3aba9442e3a8b66712b4233f5ae966d40b7e04a7ca545de6cf8149ddd800
6
+ metadata.gz: 7b25139d8cc38a6ef010df8bd0167022f39e35469d89a80c026386603224d23128d21079c193981c074bd4ff0bedf4212c826668568f36792b81479be1519b25
7
+ data.tar.gz: 5909c90e24ef0e6dc33956b1454b20b7d0f37c47c327f3923649774da7a8a26a00712195359041436d9e7c06fbbcee5256d26cd1516c9308fa51e05cd3b96a57
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.0] - 2026-10-12
6
+
7
+ ### Added
8
+
9
+ - `bin/rails public_code:status` lists every model that declares a code and whether it is addressed by code or by id.
10
+ - With `STRICT=1` the status task fails naming each model still addressed by id.
11
+
12
+ ## [0.2.0] - 2026-10-12
13
+
14
+ ### Added
15
+
16
+ - A record whose table has no code column yet is addressed and found by its id, so a gem can give its records codes before every app has migrated.
17
+ - A record whose table has the code column is not found by an all-digit address.
18
+ - `assert_no_numbered_links` fails a test naming each page that links to an address with an all-digit path segment or query value.
19
+
20
+ ### Changed
21
+
22
+ - A record gets its code when the app's secure token setting says, as with Rails' own secure tokens.
23
+
24
+ ## [0.1.0]
25
+
5
26
  ### Added
6
27
 
7
28
  - A model that calls `has_public_code` gives each new record a random 24 character code, and every address Rails builds for the record carries that code in place of its id.
data/README.md CHANGED
@@ -61,3 +61,63 @@ A column with its own name works the same way:
61
61
  ```ruby
62
62
  has_public_code :code
63
63
  ```
64
+
65
+ ## Codes are for addresses only
66
+
67
+ A code replaces the id only where a person or an outside caller sees it: page
68
+ addresses, API and MCP addresses, and links sent out. Records still point at
69
+ each other by id, and a record's id never changes.
70
+
71
+ ## A gem whose records take codes in some apps first
72
+
73
+ A gem declares the code on its model the same way:
74
+
75
+ ```ruby
76
+ module EasyFlow
77
+ class Run < ApplicationRecord
78
+ has_public_code
79
+ end
80
+ end
81
+ ```
82
+
83
+ Until an app's table has the code column, a record is addressed and found by
84
+ its id exactly as before, so a gem can release this without every app
85
+ migrating at once. Once the table has the column, every address carries the
86
+ code and an all-digit address is not found. Ship the migration that adds the
87
+ column with the gem and name it in the gem's changelog.
88
+
89
+ An app switches a gem's records to codes by copying and running that migration,
90
+ then restarting so the new column is seen.
91
+
92
+ ## Seeing which models still use ids
93
+
94
+ In a Rails app:
95
+
96
+ ```
97
+ bin/rails public_code:status
98
+ ```
99
+
100
+ It lists every model that declares a code and says whether it is addressed by
101
+ code or by id. With `STRICT=1` it fails when any model is still addressed by
102
+ id, and names each one, so a deploy or CI step can require every migration to
103
+ have run.
104
+
105
+ ## Checking pages for numbered links
106
+
107
+ In an integration test:
108
+
109
+ ```ruby
110
+ require "public_code/test_help"
111
+
112
+ class ClientAddressesTest < ActionDispatch::IntegrationTest
113
+ include PublicCode::TestHelp
114
+
115
+ test "no client page links to a numbered address" do
116
+ assert_no_numbered_links([ "/home", "/products/#{product.public_code}" ], safe: [ "page" ])
117
+ end
118
+ end
119
+ ```
120
+
121
+ It fails when a link or form on any page has an all-digit path segment, or an
122
+ all-digit query value whose name is not in `safe:`, and names each page and
123
+ link. Links to other sites, asset files and stored files are not checked.
@@ -4,9 +4,14 @@ module PublicCode
4
4
 
5
5
  class_methods do
6
6
  def has_public_code(column = :public_code)
7
- has_secure_token column, length: PublicCode::LENGTH
8
- define_method(:to_param) { public_send(column) }
9
- define_singleton_method(:find_by_public_code!) { |code| find_by!(column => code) }
7
+ PublicCode.models << self
8
+ define_singleton_method(:public_code_column?) { table_exists? && column_names.include?(column.to_s) }
9
+ define_method(:give_public_code) { self[column] ||= PublicCode.generate if self.class.public_code_column? }
10
+ define_method(:"regenerate_#{column}") { update!(column => PublicCode.generate) }
11
+ after_initialize :give_public_code, if: -> { new_record? && ActiveRecord.generate_secure_token_on == :initialize }
12
+ before_create :give_public_code
13
+ define_method(:to_param) { self.class.public_code_column? ? public_send(column) : super() }
14
+ define_singleton_method(:find_by_public_code!) { |address| public_code_column? ? find_by!(column => address) : find(address) }
10
15
  end
11
16
  end
12
17
  end
@@ -0,0 +1,5 @@
1
+ module PublicCode
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks { load File.expand_path("../tasks/public_code.rake", __dir__) }
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ require "uri"
2
+
3
+ module PublicCode
4
+ module TestHelp
5
+ def assert_no_numbered_links(pages, safe: [])
6
+ found = pages.flat_map do |page|
7
+ get page
8
+ TestHelp.numbered_links(links_on_page, safe: safe).map { |link| "#{page} links to #{link}" }
9
+ end
10
+ assert found.empty?, ([ "Pages link to numbered addresses:" ] + found).join("\n")
11
+ end
12
+
13
+ def links_on_page = css_select("a[href], form[action]").map { |element| element["href"] || element["action"] }
14
+
15
+ def self.numbered_links(addresses, safe: [])
16
+ addresses.select do |address|
17
+ next false unless own_page?(address)
18
+
19
+ uri = URI(address)
20
+ numbered_path?(uri.path) || numbered_query?(uri.query, safe)
21
+ end
22
+ end
23
+
24
+ def self.own_page?(address) = address.start_with?("/") && !address.start_with?("//", "/assets/", "/rails/active_storage/")
25
+
26
+ def self.numbered_path?(path) = path.split("/").any? { |segment| number?(segment) }
27
+
28
+ def self.numbered_query?(query, safe) = URI.decode_www_form(query.to_s).any? { |name, value| !safe.include?(name) && number?(value) }
29
+
30
+ def self.number?(value) = value.match?(/\A\d+\z/)
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module PublicCode
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/public_code.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "active_record"
2
2
  require "active_support"
3
+ require "active_support/core_ext/securerandom"
3
4
  require "public_code/version"
4
5
  require "public_code/model"
5
6
 
@@ -7,6 +8,10 @@ module PublicCode
7
8
  LENGTH = 24
8
9
 
9
10
  def self.generate = ActiveRecord::Base.generate_unique_secure_token(length: LENGTH)
11
+
12
+ def self.models = @models ||= []
10
13
  end
11
14
 
12
15
  ActiveSupport.on_load(:active_record) { include PublicCode::Model }
16
+
17
+ require "public_code/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,11 @@
1
+ namespace :public_code do
2
+ desc "List each model that declares a code and whether it is addressed by code or by id, failing on any by id when STRICT=1"
3
+ task status: :environment do
4
+ Rails.application.eager_load! if defined?(Rails.application) && Rails.application
5
+ models = PublicCode.models.sort_by(&:name)
6
+ models.each { |model| puts "#{model.name} addressed by #{model.public_code_column? ? "code" : "id"}" }
7
+
8
+ by_id = models.reject(&:public_code_column?)
9
+ abort "Still addressed by id: #{by_id.map(&:name).join(", ")}" if ENV["STRICT"] == "1" && by_id.any?
10
+ end
11
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: public_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -50,7 +50,10 @@ files:
50
50
  - README.md
51
51
  - lib/public_code.rb
52
52
  - lib/public_code/model.rb
53
+ - lib/public_code/railtie.rb
54
+ - lib/public_code/test_help.rb
53
55
  - lib/public_code/version.rb
56
+ - lib/tasks/public_code.rake
54
57
  homepage: https://github.com/DYB-Development/public_code
55
58
  licenses:
56
59
  - MIT