Skip to content

Commit 93061e9

Browse files
committed
Refactor feed creation and token internals
1 parent 441e63e commit 93061e9

22 files changed

Lines changed: 416 additions & 415 deletions

Gemfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ source 'https://rubygems.org'
44

55
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
66

7-
gem 'html2rss', '~> 0.18'
8-
# gem 'html2rss', github: 'html2rss/html2rss', branch: :master
7+
# gem 'html2rss', '~> 0.18'
8+
gem 'html2rss', github: 'html2rss/html2rss', branch: 'codex-pr-auto-fallback-pipeline'
99
gem 'html2rss-configs', github: 'html2rss/html2rss-configs'
1010

1111
# Use these instead of the two above (uncomment them) when developing locally:
1212
# gem 'html2rss', path: '../html2rss'
1313
# gem 'html2rss-configs', path: '../html2rss-configs'
1414

15+
gem 'concurrent-ruby'
1516
gem 'parallel'
1617
gem 'rack-cache'
1718
gem 'rack-timeout'
@@ -21,7 +22,6 @@ gem 'zeitwerk'
2122
gem 'puma', require: false
2223

2324
group :development do
24-
gem 'byebug'
2525
gem 'irb', require: false
2626
gem 'rake', require: false
2727
gem 'rubocop', require: false

Gemfile.lock

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
GIT
2+
remote: https://github.com/html2rss/html2rss
3+
revision: 54f6f53482352932b69621f1df989f83cd285c72
4+
branch: codex-pr-auto-fallback-pipeline
5+
specs:
6+
html2rss (0.18.0)
7+
addressable (~> 2.7)
8+
brotli
9+
dry-validation
10+
faraday (> 2.0.1, < 3.0)
11+
faraday-follow_redirects
12+
faraday-gzip (~> 3)
13+
kramdown
14+
mime-types (> 3.0)
15+
nokogiri (>= 1.10, < 2.0)
16+
parallel
17+
puppeteer-ruby
18+
regexp_parser
19+
reverse_markdown (~> 3.0)
20+
rss
21+
sanitize
22+
thor
23+
tzinfo
24+
zeitwerk
25+
126
GIT
227
remote: https://github.com/html2rss/html2rss-configs
328
revision: 71981aff28d88e4553c206d78bf54d5633bcdd19
@@ -68,8 +93,6 @@ GEM
6893
bigdecimal (4.1.2)
6994
brotli (0.8.0)
7095
builder (3.3.0)
71-
byebug (13.0.0)
72-
reline (>= 0.6.0)
7396
climate_control (1.2.0)
7497
concurrent-ruby (1.3.6)
7598
connection_pool (3.0.2)
@@ -138,25 +161,6 @@ GEM
138161
fiber-storage
139162
fiber-storage (1.0.1)
140163
hashdiff (1.2.1)
141-
html2rss (0.18.0)
142-
addressable (~> 2.7)
143-
brotli
144-
dry-validation
145-
faraday (> 2.0.1, < 3.0)
146-
faraday-follow_redirects
147-
faraday-gzip (~> 3)
148-
kramdown
149-
mime-types (> 3.0)
150-
nokogiri (>= 1.10, < 2.0)
151-
parallel
152-
puppeteer-ruby
153-
regexp_parser
154-
reverse_markdown (~> 3.0)
155-
rss
156-
sanitize
157-
thor
158-
tzinfo
159-
zeitwerk
160164
i18n (1.14.8)
161165
concurrent-ruby (~> 1.0)
162166
io-console (0.8.2)
@@ -371,9 +375,9 @@ PLATFORMS
371375
x86_64-linux-musl
372376

373377
DEPENDENCIES
374-
byebug
375378
climate_control
376-
html2rss (~> 0.18)
379+
concurrent-ruby
380+
html2rss!
377381
html2rss-configs!
378382
irb
379383
parallel
@@ -413,7 +417,6 @@ CHECKSUMS
413417
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
414418
brotli (0.8.0) sha256=0c5a42046b3b603fb109656881147fd76064c034b7d19c1b4fcc32a093a4d55d
415419
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
416-
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
417420
climate_control (1.2.0) sha256=36b21896193fa8c8536fa1cd843a07cf8ddbd03aaba43665e26c53ec1bd70aa5
418421
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
419422
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
@@ -442,7 +445,7 @@ CHECKSUMS
442445
fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06
443446
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
444447
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
445-
html2rss (0.18.0) sha256=83e9dd0388d65f1992df4afc9d345cbfd84e8c740be3756815b5e840ac71cf54
448+
html2rss (0.18.0)
446449
html2rss-configs (0.2.0)
447450
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
448451
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc

app/web/api/v1/create_feed.rb

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
# frozen_string_literal: true
22

3-
require 'time'
43
require 'json'
4+
require 'time'
55

66
module Html2rss
77
module Web
88
module Api
99
module V1
1010
##
1111
# Creates stable feed records from authenticated API requests.
12-
module CreateFeed # rubocop:disable Metrics/ModuleLength
12+
module CreateFeed
1313
FEED_ATTRIBUTE_KEYS =
1414
%i[id name url feed_token public_url json_public_url created_at updated_at].freeze
15-
class << self # rubocop:disable Metrics/ClassLength
15+
16+
class << self
1617
# Creates a feed and returns a normalized API success payload.
1718
#
1819
# @param request [Rack::Request] HTTP request with auth context.
1920
# @return [Hash{Symbol=>Object}] API response payload.
20-
def call(request) # rubocop:disable Metrics/MethodLength
21-
params, feed_data = build_feed_from_request(request)
21+
# rubocop:disable Metrics/MethodLength
22+
def call(request)
23+
account = require_account(request)
24+
params = build_create_params(request, account)
25+
feed_data = create_feed(params, account)
26+
2227
emit_create_success(params)
2328
Response.success(response: request.response,
2429
status: 201,
@@ -30,14 +35,10 @@ def call(request) # rubocop:disable Metrics/MethodLength
3035
emit_create_failure(error)
3136
raise
3237
end
38+
# rubocop:enable Metrics/MethodLength
3339

3440
private
3541

36-
# @return [void]
37-
def ensure_auto_source_enabled!
38-
raise Html2rss::Web::AutoSourceDisabledError unless AutoSource.enabled?
39-
end
40-
4142
# @param request [Rack::Request]
4243
# @return [Hash]
4344
def require_account(request)
@@ -47,15 +48,41 @@ def require_account(request)
4748
account
4849
end
4950

50-
# @param params [Hash]
51+
# @param request [Rack::Request]
5152
# @param account [Hash]
5253
# @return [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
53-
def build_create_params(params, account)
54-
url = validated_url(params['url'], account)
55-
FeedMetadata::CreateParams.new(
56-
url: url,
57-
name: FeedMetadata.site_title_for(url)
58-
)
54+
def build_create_params(request, account)
55+
url = validated_url(request_params(request)['url'], account)
56+
FeedMetadata::CreateParams.new(url:, name: FeedMetadata.site_title_for(url))
57+
end
58+
59+
# @param request [Rack::Request]
60+
# @return [Hash]
61+
def request_params(request)
62+
return request.params unless json_request?(request)
63+
64+
request.GET.merge(parsed_json_body(request))
65+
end
66+
67+
# @param request [Rack::Request]
68+
# @return [Hash]
69+
def parsed_json_body(request)
70+
raw_body = request.body.read
71+
request.body.rewind
72+
return {} if raw_body.strip.empty?
73+
74+
parsed = JSON.parse(raw_body)
75+
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload' unless parsed.is_a?(Hash)
76+
77+
parsed
78+
rescue JSON::ParserError
79+
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload'
80+
end
81+
82+
# @param request [Rack::Request]
83+
# @return [Boolean]
84+
def json_request?(request)
85+
request.env['CONTENT_TYPE'].to_s.include?('application/json')
5986
end
6087

6188
# @param raw_url [String, nil]
@@ -102,49 +129,23 @@ def hostname_input?(url)
102129
}ix.match?(url)
103130
end
104131

105-
# @param feed_data [Hash, Html2rss::Web::Api::V1::FeedMetadata::Metadata]
106-
# @return [Hash{Symbol=>Object}]
107-
def feed_attributes(feed_data)
108-
timestamp = Time.now.iso8601
109-
typed_feed = feed_metadata(feed_data)
110-
typed_feed_attributes(typed_feed, timestamp).slice(*FEED_ATTRIBUTE_KEYS)
111-
end
112-
113-
# @param request [Rack::Request]
114-
# @return [Hash]
115-
def request_params(request)
116-
return request.params unless json_request?(request)
117-
118-
raw_body = request.body.read
119-
request.body.rewind
120-
return request.params if raw_body.strip.empty?
121-
122-
parsed = JSON.parse(raw_body)
123-
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload' unless parsed.is_a?(Hash)
124-
125-
request.params.merge(parsed)
126-
rescue JSON::ParserError
127-
raise Html2rss::Web::BadRequestError, 'Invalid JSON payload'
128-
end
129-
130-
# @param request [Rack::Request]
131-
# @return [Boolean]
132-
def json_request?(request)
133-
content_type = request.env['CONTENT_TYPE'].to_s
134-
content_type.include?('application/json')
135-
end
136-
137-
# @param request [Rack::Request]
138-
# @return [Array<(Html2rss::Web::Api::V1::FeedMetadata::CreateParams, Object)>]
139-
def build_feed_from_request(request)
140-
account = require_account(request)
141-
ensure_auto_source_enabled!
142-
params = build_create_params(request_params(request), account)
132+
# @param params [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
133+
# @param account [Hash]
134+
# @return [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
135+
def create_feed(params, account)
136+
raise Html2rss::Web::AutoSourceDisabledError unless AutoSource.enabled?
143137

144138
feed_data = AutoSource.create_stable_feed(params.name, params.url, account)
145139
raise Html2rss::Web::InternalServerError, 'Failed to create feed' unless feed_data
146140

147-
[params, feed_data]
141+
feed_data.is_a?(FeedMetadata::Metadata) ? feed_data : FeedMetadata::Metadata.new(**feed_data)
142+
end
143+
144+
# @param feed_data [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
145+
# @return [Hash{Symbol=>Object}]
146+
def feed_attributes(feed_data)
147+
timestamp = Time.now.iso8601
148+
feed_data.to_h.merge(created_at: timestamp, updated_at: timestamp).slice(*FEED_ATTRIBUTE_KEYS)
148149
end
149150

150151
# @param params [Html2rss::Web::Api::V1::FeedMetadata::CreateParams]
@@ -168,21 +169,6 @@ def emit_create_failure(error)
168169
level: :warn
169170
)
170171
end
171-
172-
# @param feed_data [Hash, Html2rss::Web::Api::V1::FeedMetadata::Metadata]
173-
# @return [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
174-
def feed_metadata(feed_data)
175-
return feed_data if feed_data.is_a?(FeedMetadata::Metadata)
176-
177-
FeedMetadata::Metadata.new(**feed_data)
178-
end
179-
180-
# @param typed_feed [Html2rss::Web::Api::V1::FeedMetadata::Metadata]
181-
# @param timestamp [String]
182-
# @return [Hash{Symbol=>Object}]
183-
def typed_feed_attributes(typed_feed, timestamp)
184-
typed_feed.to_h.merge(created_at: timestamp, updated_at: timestamp)
185-
end
186172
end
187173
end
188174
end

app/web/boot.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,33 @@ module Web
77
##
88
# Boot helpers for code loading and runtime setup.
99
module Boot
10+
@mutex = Mutex.new
11+
@setup = false
12+
@loader = nil
13+
1014
class << self
1115
# @param reloadable [Boolean]
1216
# @return [Zeitwerk::Loader]
1317
def setup!(reloadable: false)
14-
return loader if setup?
18+
@mutex.synchronize do
19+
return @loader if @setup
1520

16-
loader.enable_reloading if reloadable
17-
loader.setup
18-
@setup = true # rubocop:disable ThreadSafety/ClassInstanceVariable
19-
loader
21+
@loader ||= build_loader
22+
@loader.enable_reloading if reloadable
23+
@loader.setup
24+
@setup = true
25+
@loader
26+
end
2027
end
2128

2229
# @return [Zeitwerk::Loader]
2330
def loader
24-
@loader ||= build_loader # rubocop:disable ThreadSafety/ClassInstanceVariable
31+
@mutex.synchronize { @loader ||= build_loader }
2532
end
2633

2734
# @return [Boolean]
2835
def setup?
29-
# Loader setup happens once during process boot.
30-
# rubocop:disable ThreadSafety/ClassInstanceVariable
31-
@setup == true
32-
# rubocop:enable ThreadSafety/ClassInstanceVariable
36+
@mutex.synchronize { @setup == true }
3337
end
3438

3539
# @return [void]

app/web/config/local_config.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ module Web
1616
# Keeping lookup/defaulting here gives the rest of the app one predictable
1717
# config shape instead of repeating file parsing and fallback logic.
1818
module LocalConfig
19+
@mutex = Mutex.new
20+
@snapshot = nil
21+
1922
##
2023
# raised when the local config wasn't found
2124
class NotFound < RuntimeError; end
@@ -63,9 +66,7 @@ def yaml
6366
##
6467
# @return [Html2rss::Web::ConfigSnapshot::Snapshot]
6568
def snapshot
66-
return @snapshot if @snapshot # rubocop:disable ThreadSafety/ClassInstanceVariable
67-
68-
@snapshot = ConfigSnapshot.load(yaml) # rubocop:disable ThreadSafety/ClassInstanceVariable
69+
@mutex.synchronize { @snapshot ||= ConfigSnapshot.load(yaml) }
6970
rescue KeyError, TypeError, ArgumentError => error
7071
raise InvalidConfig, "Invalid local config: #{error.message}"
7172
end
@@ -74,7 +75,7 @@ def snapshot
7475
# @param reason [String]
7576
# @return [nil]
7677
def reload!(reason: 'manual')
77-
@snapshot = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
78+
@mutex.synchronize { @snapshot = nil }
7879
SecurityLogger.log_cache_lifecycle('local_config', 'reload', reason: reason)
7980
nil
8081
end

0 commit comments

Comments
 (0)