-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathanalysis.cr
More file actions
240 lines (212 loc) · 8.41 KB
/
Copy pathanalysis.cr
File metadata and controls
240 lines (212 loc) · 8.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
require "json"
require "uuid"
require "../config/database"
require "../services/log_service"
module Tokei::Api::Models
# Model class representing repository analysis results
class Analysis
EMPTY_RESULT_JSON = JSON.parse("{}")
property id : UUID?
property repo_url : String
property analyzed_at : Time?
property result : JSON::Any
# Badge statistics properties
property total_lines : Int32?
property total_code : Int32?
property total_comments : Int32?
property total_blanks : Int32?
property top_language : String?
property top_language_lines : Int32?
property language_count : Int32?
property code_comment_ratio : Float64?
def initialize(@repo_url : String, result : String | JSON::Any)
@id = nil
@analyzed_at = nil
# Parse as JSON if string, use as is if JSON::Any
@result = result.is_a?(String) ? JSON.parse(result) : result
# Extract statistics from result
extract_stats_from_result
end
ANALYSIS_COLUMNS = "id, repo_url, analyzed_at, result, total_lines, total_code, total_comments, total_blanks, top_language, top_language_lines, language_count, code_comment_ratio"
ANALYSIS_SUMMARY_COLUMNS = "id, repo_url, analyzed_at, total_lines, total_code, total_comments, total_blanks, top_language, top_language_lines, language_count, code_comment_ratio"
def self.timestamp(time : Time) : String
time.to_rfc3339(fraction_digits: 0)
end
private def self.parse_timestamp(value : String) : Time
Time.parse_rfc3339(value)
end
private def self.read_optional_i32(result_set) : Int32?
result_set.read(Int64?).try(&.to_i32)
end
private def self.read_summary_record(result_set) : Analysis
id_value = UUID.new(result_set.read(String))
repo_url = result_set.read(String)
analyzed_at = parse_timestamp(result_set.read(String))
record = Analysis.new(repo_url, EMPTY_RESULT_JSON)
record.id = id_value
record.analyzed_at = analyzed_at
read_stats(result_set, record)
record
end
private def self.read_full_record(result_set) : Analysis
id_value = UUID.new(result_set.read(String))
repo_url = result_set.read(String)
analyzed_at = parse_timestamp(result_set.read(String))
result = JSON.parse(result_set.read(String))
record = Analysis.new(repo_url, result)
record.id = id_value
record.analyzed_at = analyzed_at
read_stats(result_set, record)
record
end
private def self.read_stats(result_set, record : Analysis) : Nil
record.total_lines = read_optional_i32(result_set)
record.total_code = read_optional_i32(result_set)
record.total_comments = read_optional_i32(result_set)
record.total_blanks = read_optional_i32(result_set)
record.top_language = result_set.read(String?)
record.top_language_lines = read_optional_i32(result_set)
record.language_count = read_optional_i32(result_set)
record.code_comment_ratio = result_set.read(Float64?)
end
# Delete analyses older than the retention period (default: 7 days)
# Can be configured via RETENTION_DAYS environment variable
def self.cleanup_old_data : Int64
# Get retention days from environment variable or use default (7 days)
retention_days = ENV["RETENTION_DAYS"]?.try(&.to_i?) || 7
conn = Tokei::Api::Config::Database.connection
begin
result = conn.exec("DELETE FROM analyses WHERE analyzed_at < ?", timestamp(Time.utc - retention_days.days))
result.rows_affected
ensure
conn.close
end
end
# Extract statistics from result JSON for badge generation
private def extract_stats_from_result
total_code = 0
total_comments = 0
total_blanks = 0
top_language = ""
top_language_lines = 0
language_count = 0
@result.as_h.each do |language, stats|
next if language == "Total"
stats_obj = stats.as_h
code = stats_obj["code"]?.try(&.as_i) || 0
comments = stats_obj["comments"]?.try(&.as_i) || 0
blanks = stats_obj["blanks"]?.try(&.as_i) || 0
total_code += code
total_comments += comments
total_blanks += blanks
language_count += 1
if code > top_language_lines
top_language = language
top_language_lines = code
end
end
@total_code = total_code
@total_comments = total_comments
@total_blanks = total_blanks
@total_lines = total_code + total_comments + total_blanks
@top_language = top_language
@top_language_lines = top_language_lines
@language_count = language_count
@code_comment_ratio = total_comments > 0 ? (total_code.to_f / total_comments) : 0.0
end
# Save to database
def save
conn = Tokei::Api::Config::Database.connection
begin
if id.nil?
now = Time.utc
@id = UUID.random
@analyzed_at = now
# Create new with statistics
conn.exec(
"INSERT INTO analyses (id, repo_url, analyzed_at, result, total_lines, total_code, total_comments, total_blanks, top_language, top_language_lines, language_count, code_comment_ratio) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
@id.to_s, @repo_url, self.class.timestamp(now), @result.to_json, @total_lines, @total_code, @total_comments, @total_blanks, @top_language, @top_language_lines, @language_count, @code_comment_ratio
)
else
# Update with statistics
conn.exec(
"UPDATE analyses SET repo_url = ?, result = ?, total_lines = ?, total_code = ?, total_comments = ?, total_blanks = ?, top_language = ?, top_language_lines = ?, language_count = ?, code_comment_ratio = ? WHERE id = ?",
@repo_url, @result.to_json, @total_lines, @total_code, @total_comments, @total_blanks, @top_language, @top_language_lines, @language_count, @code_comment_ratio, @id.to_s
)
end
true
rescue ex
Tokei::Api::Services::LogService.error_exception("analysis.save.failed", ex, {
"repo_url" => Tokei::Api::Services::LogService.mask_url(@repo_url),
"id" => @id.try(&.to_s) || "",
})
false
ensure
conn.close
end
end
# Search latest analysis by repository URL without loading large result JSON.
# Useful for summary/badge flows where only precomputed stats are needed.
def self.find_latest_by_repo_url(repo_url : String) : Analysis?
conn = Tokei::Api::Config::Database.connection
begin
analysis = nil
conn.query("SELECT #{ANALYSIS_SUMMARY_COLUMNS} FROM analyses WHERE repo_url = ? ORDER BY analyzed_at DESC LIMIT 1", repo_url) do |result_set|
if result_set.move_next
analysis = read_summary_record(result_set)
end
end
analysis
ensure
conn.close
end
end
# Search latest analysis by repository URL with the full result JSON.
# Useful for flows that need language breakdown data.
def self.find_latest_full_by_repo_url(repo_url : String) : Analysis?
conn = Tokei::Api::Config::Database.connection
begin
analysis = nil
conn.query("SELECT #{ANALYSIS_COLUMNS} FROM analyses WHERE repo_url = ? ORDER BY analyzed_at DESC LIMIT 1", repo_url) do |result_set|
if result_set.move_next
analysis = read_full_record(result_set)
end
end
analysis
ensure
conn.close
end
end
# Search by ID without loading large result JSON.
# Useful for badge endpoint where only stats are required.
def self.find_summary_by_id(id : String) : Analysis?
conn = Tokei::Api::Config::Database.connection
begin
analysis = nil
conn.query("SELECT #{ANALYSIS_SUMMARY_COLUMNS} FROM analyses WHERE id = ?", id) do |result_set|
if result_set.move_next
analysis = read_summary_record(result_set)
end
end
analysis
ensure
conn.close
end
end
# Search by ID
def self.find(id : String) : Analysis?
conn = Tokei::Api::Config::Database.connection
begin
analysis = nil
conn.query("SELECT #{ANALYSIS_COLUMNS} FROM analyses WHERE id = ?", id) do |result_set|
if result_set.move_next
analysis = read_full_record(result_set)
end
end
analysis
ensure
conn.close
end
end
end
end