hyraft 0.1.0.alpha1

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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +231 -0
  6. data/exe/hyraft +5 -0
  7. data/lib/hyraft/boot/asset_preloader.rb +185 -0
  8. data/lib/hyraft/boot/preloaded_static.rb +46 -0
  9. data/lib/hyraft/boot/preloader.rb +206 -0
  10. data/lib/hyraft/cli.rb +187 -0
  11. data/lib/hyraft/compiler/compiler.rb +34 -0
  12. data/lib/hyraft/compiler/html_purifier.rb +181 -0
  13. data/lib/hyraft/compiler/javascript_library.rb +281 -0
  14. data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
  15. data/lib/hyraft/compiler/parser.rb +27 -0
  16. data/lib/hyraft/compiler/renderer.rb +217 -0
  17. data/lib/hyraft/engine/circuit.rb +35 -0
  18. data/lib/hyraft/engine/port.rb +17 -0
  19. data/lib/hyraft/engine/source.rb +19 -0
  20. data/lib/hyraft/engine.rb +11 -0
  21. data/lib/hyraft/router/api_router.rb +65 -0
  22. data/lib/hyraft/router/web_router.rb +136 -0
  23. data/lib/hyraft/system_info.rb +26 -0
  24. data/lib/hyraft/version.rb +5 -0
  25. data/lib/hyraft.rb +48 -0
  26. data/templates/do_app/Gemfile +50 -0
  27. data/templates/do_app/Rakefile +88 -0
  28. data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
  29. data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
  30. data/templates/do_app/boot.rb +41 -0
  31. data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
  32. data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
  33. data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
  34. data/templates/do_app/framework/errors/error_handler.rb +75 -0
  35. data/templates/do_app/framework/errors/templates/304.html +22 -0
  36. data/templates/do_app/framework/errors/templates/400.html +22 -0
  37. data/templates/do_app/framework/errors/templates/401.html +22 -0
  38. data/templates/do_app/framework/errors/templates/403.html +22 -0
  39. data/templates/do_app/framework/errors/templates/404.html +62 -0
  40. data/templates/do_app/framework/errors/templates/500.html +73 -0
  41. data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
  42. data/templates/do_app/infra/config/environment.rb +86 -0
  43. data/templates/do_app/infra/config/error_config.rb +80 -0
  44. data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
  45. data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
  46. data/templates/do_app/infra/database/sequel_connection.rb +62 -0
  47. data/templates/do_app/infra/gems/database.rb +7 -0
  48. data/templates/do_app/infra/gems/load_all.rb +4 -0
  49. data/templates/do_app/infra/gems/utilities.rb +1 -0
  50. data/templates/do_app/infra/gems/web.rb +3 -0
  51. data/templates/do_app/infra/server/api-server.ru +13 -0
  52. data/templates/do_app/infra/server/web-server.ru +32 -0
  53. data/templates/do_app/package.json +9 -0
  54. data/templates/do_app/public/favicon.ico +0 -0
  55. data/templates/do_app/public/icons/docs.svg +10 -0
  56. data/templates/do_app/public/icons/expli.svg +13 -0
  57. data/templates/do_app/public/icons/git-repo.svg +13 -0
  58. data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
  59. data/templates/do_app/public/icons/template-engine.svg +26 -0
  60. data/templates/do_app/public/images/hyr-logo.png +0 -0
  61. data/templates/do_app/public/images/hyr-logo.webp +0 -0
  62. data/templates/do_app/public/index.html +22 -0
  63. data/templates/do_app/public/styles/css/main.css +418 -0
  64. data/templates/do_app/public/styles/css/spa.css +171 -0
  65. data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
  66. data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
  67. data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
  68. data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
  69. data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
  70. data/templates/do_app/test/db.rb +106 -0
  71. data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
  72. data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
  73. data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
  74. data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
  75. data/templates/do_app/test/integration/database/migration_test.rb +35 -0
  76. data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
  77. data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
  78. data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
  79. data/templates/do_app/test/support/test_patches.rb +33 -0
  80. data/templates/do_app/test/test_helper.rb +526 -0
  81. data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
  82. data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
  83. data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
  84. metadata +291 -0
@@ -0,0 +1,418 @@
1
+
2
+ body {
3
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif !important;
4
+ background: #f8f9fa !important;
5
+ margin: 0 !important;
6
+ min-height: 100vh !important;
7
+ all: unset;
8
+ }
9
+
10
+
11
+ /* === Logo Styling === */
12
+ picture {
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: center;
16
+ margin: 2rem auto;
17
+ text-align: center;
18
+ }
19
+
20
+
21
+
22
+
23
+ /* Logo inside the gradient container */
24
+ .logo {
25
+ display: block;
26
+ max-width: 200px;
27
+ width: 100%;
28
+ height: auto;
29
+ margin: 0 auto;
30
+ border-radius: 12px;
31
+ animation:bounce 3s infinite alternate;
32
+ }
33
+
34
+ /* === Animation keyframes === */
35
+ @keyframes bounce { 0%{transform:translateY(0);}
36
+ 25%{transform:translateY(-1px);}
37
+ 50%{transform:translateY(-5px);}
38
+ 75%{transform:translateY(1px);}
39
+ 100%{transform:translateY(0);}
40
+ }
41
+
42
+ .icon-home-feature {
43
+ display: block;
44
+ max-width: 50px;
45
+ width: 100%;
46
+ height: auto;
47
+ margin: 0 auto;
48
+ border-radius: 12px;
49
+ animation:bounce 3s infinite alternate;
50
+ filter: invert(22%) sepia(92%) saturate(7484%) hue-rotate(358deg) brightness(100%) contrast(105%);
51
+ }
52
+
53
+ .icon-home-feature :hover {
54
+ background-color: #000080;
55
+ color: #991010;
56
+ }
57
+
58
+ .icon-home-feature img {
59
+ filter: invert(22%) sepia(92%) saturate(7484%) hue-rotate(358deg) brightness(100%) contrast(105%);
60
+ transition: filter 0.3s ease;
61
+ }
62
+
63
+ .icon-home-feature:hover img {
64
+ filter: invert(49%) sepia(83%) saturate(600%) hue-rotate(300deg); /* example pinkish */
65
+ }
66
+ .component-name {
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ padding: 2rem;
71
+ background: linear-gradient(135deg, #eb733c 0%, #0437F2 100%);
72
+ min-height: 100vh;
73
+ display: flex;
74
+ align-items: center;
75
+ }
76
+
77
+ .component {
78
+ background: white;
79
+ border-radius: 12px;
80
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
81
+ padding: 3rem;
82
+ border: 2px solid #e0e0e0;
83
+ text-align: center;
84
+ max-width: 500px;
85
+ width: 100%;
86
+
87
+ font-size: 3.5rem;
88
+ color: #e9475c;
89
+ font-weight: 600;
90
+ margin-bottom: 1rem;
91
+
92
+ }
93
+
94
+ .main-container {
95
+ max-width: 1000px !important;
96
+ margin: 0 auto !important;
97
+ padding: 2rem !important;
98
+ }
99
+
100
+ .main-header {
101
+ text-align: center !important;
102
+ color: #E13026 !important;
103
+ margin-bottom: 0.5rem !important;
104
+ }
105
+
106
+ .main-header h1 {
107
+ text-align: center !important;
108
+ color: #E13026 !important;
109
+ margin-bottom: 0.5rem !important;
110
+ font-family:Verdana, Geneva, Tahoma, sans-serif;
111
+ }
112
+
113
+ .home-tagline {
114
+ margin-top: 20px;
115
+ font-size: 40px;
116
+ color:#000080 !important;
117
+ font-weight: 600 !important;
118
+ margin-bottom: 0.2rem !important;
119
+ }
120
+
121
+
122
+
123
+ .status {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ gap: 0.5rem;
127
+ background: #ecf0f1;
128
+ padding: 0.75rem 1.5rem !important;
129
+ border-radius: 25px;
130
+ margin-bottom: 2rem;
131
+ font-weight: 500;
132
+ }
133
+
134
+ .status-dot {
135
+ width: 8px;
136
+ height: 8px;
137
+ background: #27ae60;
138
+ border-radius: 50%;
139
+ animation: pulse 2s infinite;
140
+ }
141
+
142
+ @keyframes pulse {
143
+ 0%, 100% { opacity: 1; }
144
+ 50% { opacity: 0.5; }
145
+ }
146
+
147
+ .actions {
148
+ display: flex;
149
+ gap: 1rem;
150
+ justify-content: center;
151
+ margin-bottom: 4rem;
152
+ flex-wrap: wrap;
153
+ }
154
+
155
+ .btn {
156
+ padding: 0.75rem 1.5rem;
157
+ background: #95a5a6;
158
+ color: white;
159
+ text-decoration: none;
160
+ border-radius: 6px;
161
+ border: none;
162
+ cursor: pointer;
163
+ transition: all 0.2s;
164
+ }
165
+
166
+ .btn.primary {
167
+ background: #3498db;
168
+ }
169
+
170
+ .btn:hover {
171
+ transform: translateY(-2px);
172
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
173
+ }
174
+
175
+ .main-feature {
176
+ text-align: center !important;
177
+ color: #2c3e50 !important;
178
+ margin-bottom: 3rem !important;
179
+ }
180
+
181
+ .home-subtitle {
182
+ margin-top: 10px;
183
+ text-align: center;
184
+ color: #7f8c8d;
185
+ margin-bottom: 3rem !important;
186
+ }
187
+
188
+
189
+ .home-feature-grid {
190
+ display: grid !important;
191
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
192
+ gap: 2rem;
193
+ }
194
+
195
+ .home-feature {
196
+ background: white;
197
+ padding: 2rem;
198
+ border-radius: 8px;
199
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
200
+ text-align: center;
201
+ }
202
+
203
+ .home-feature-icon {
204
+ font-size: 2.5rem;
205
+ margin-bottom: 1rem;
206
+ }
207
+
208
+ .home-feature h3 {
209
+ font-size: 17px !important;
210
+ font-weight: bold !important;
211
+ color: #000080 !important;
212
+ margin-bottom: 1rem !important;
213
+ all: unset;
214
+ }
215
+
216
+ .home-feature p {
217
+ margin-top: 5px !important;
218
+ color: #7f8c8d !important;
219
+ line-height: 1.5 !important;
220
+ }
221
+
222
+ .demo {
223
+ background: white;
224
+ padding: 2rem;
225
+ border-radius: 8px;
226
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
227
+ text-align: center;
228
+ margin-top: 2rem;
229
+ }
230
+
231
+ .demo h3 {
232
+ color: #2c3e50;
233
+ margin-bottom: 0.5rem;
234
+ }
235
+
236
+ .demo p {
237
+ color: #7f8c8d;
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+
242
+ /* Footer Styles */
243
+ .site-footer {
244
+ background: linear-gradient(135deg, #E13026 0%, #000080 100%);
245
+ color: #fff;
246
+ padding: 40px 20px;
247
+ text-align: center;
248
+ }
249
+
250
+ .footer-container {
251
+ max-width: 1200px;
252
+ margin: 0 auto;
253
+ }
254
+
255
+ .footer-info h5, .footer-links h5 {
256
+ margin-bottom: 10px;
257
+ font-size: 18px;
258
+ }
259
+
260
+ .footer-links .social-buttons {
261
+ display: flex;
262
+ justify-content: center;
263
+ gap: 15px;
264
+ flex-wrap: wrap;
265
+ margin-top: 10px;
266
+ }
267
+
268
+ .social-btn {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 6px;
272
+ color: #ffffff;
273
+ text-decoration: none;
274
+ padding: 6px 12px;
275
+ border-radius: 5px;
276
+
277
+ transition: background 0.2s ease;
278
+ }
279
+
280
+ .social-btn:hover {
281
+ background-color: #000080;
282
+ color: #ffffff;
283
+ }
284
+
285
+ .social-btn img {
286
+ filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg);
287
+ transition: filter 0.3s ease;
288
+ }
289
+
290
+ .social-btn:hover img {
291
+ filter: invert(49%) sepia(83%) saturate(600%) hue-rotate(300deg); /* example pinkish */
292
+ }
293
+
294
+ .icon-footer {
295
+ width: 20px;
296
+ height: 20px;
297
+ }
298
+
299
+ .footer-meta {
300
+ margin-top: 30px;
301
+ font-size: 14px;
302
+ color: #aaa;
303
+ }
304
+
305
+ @media (max-width: 768px) {
306
+ .logo {
307
+ max-width: 250px;
308
+ }
309
+ .main-container {
310
+ padding: 1rem;
311
+ }
312
+
313
+ .main-header h1 {
314
+ font-size: 2.5rem;
315
+ }
316
+
317
+ .actions {
318
+ flex-direction: column;
319
+ align-items: center;
320
+ }
321
+
322
+ .btn {
323
+ width: 200px;
324
+ }
325
+
326
+ .home-feature-grid {
327
+ grid-template-columns: 1fr;
328
+ }
329
+ }
330
+
331
+
332
+
333
+
334
+ /* Card Styles */
335
+ .counter-card{
336
+ background: #f8f9fa;
337
+ border: 2px solid #e9ecef;
338
+ border-radius: 15px;
339
+ padding: 25px;
340
+ transition: all 0.3s ease;
341
+ position: relative;
342
+ overflow: hidden;
343
+ }
344
+
345
+ .counter-card::before {
346
+ content: '';
347
+ position: absolute;
348
+ top: 0;
349
+ left: 0;
350
+ right: 0;
351
+ height: 4px;
352
+ background: linear-gradient(90deg, #667eea, #764ba2);
353
+ }
354
+
355
+ .counter-card:hover {
356
+ transform: translateY(-5px);
357
+ box-shadow: 0 10px 25px rgba(0,0,0,0.1);
358
+ border-color: #667eea;
359
+ }
360
+
361
+ .counter-card h3{
362
+ margin-top: 0;
363
+ margin-bottom: 20px;
364
+ color: #000080;
365
+ font-size: 1.3rem;
366
+ border-bottom: 2px solid #e9ecef;
367
+ padding-bottom: 10px;
368
+ }
369
+
370
+ /* Counter Styles */
371
+ .counter {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+ gap: 15px;
376
+ margin-bottom: 15px;
377
+ }
378
+
379
+ .counter-btn {
380
+ padding: 12px 20px;
381
+ background: linear-gradient(135deg, #667eea, #000080);
382
+ color: white;
383
+ border: none;
384
+ border-radius: 50%;
385
+ width: 50px;
386
+ height: 50px;
387
+ font-size: 1.5rem;
388
+ font-weight: bold;
389
+ cursor: pointer;
390
+ transition: all 0.3s ease;
391
+ display: flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ }
395
+
396
+ .counter-btn:hover {
397
+ transform: scale(1.1);
398
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
399
+ }
400
+
401
+ .count-display {
402
+ font-size: 2.5rem;
403
+ font-weight: bold;
404
+ color: #2c3e50;
405
+ min-width: 80px;
406
+ text-align: center;
407
+ padding: 10px 20px;
408
+
409
+ }
410
+
411
+
412
+ .status-text {
413
+ display: block;
414
+ font-size: 1.1rem;
415
+ color: #000000;
416
+ margin-bottom: 15px;
417
+ padding: 10px;
418
+ }
@@ -0,0 +1,171 @@
1
+ .app {
2
+ max-width: 800px;
3
+ margin: 0 auto;
4
+ padding: 20px;
5
+ font-family: Arial, sans-serif;
6
+ }
7
+
8
+ .app-header {
9
+ border-bottom: 2px solid #333;
10
+ padding-bottom: 20px;
11
+ margin-bottom: 30px;
12
+ }
13
+
14
+ .app-nav {
15
+ display: flex;
16
+ gap: 10px;
17
+ margin-top: 15px;
18
+ }
19
+
20
+ .nav-btn, .back-btn, .action-btn, .submit-btn, .error-btn {
21
+ padding: 8px 16px;
22
+ border: none;
23
+ border-radius: 4px;
24
+ cursor: pointer;
25
+ font-size: 14px;
26
+ }
27
+
28
+ .nav-btn, .submit-btn {
29
+ background: #007bff;
30
+ color: white;
31
+ }
32
+
33
+ .back-btn {
34
+ background: #6c757d;
35
+ color: white;
36
+ }
37
+
38
+ .action-btn {
39
+ background: #28a745;
40
+ color: white;
41
+ margin: 2px;
42
+ }
43
+
44
+ .edit-btn {
45
+ background: #ffc107;
46
+ color: black;
47
+ }
48
+
49
+ .delete-btn {
50
+ background: #dc3545;
51
+ color: white;
52
+ }
53
+
54
+ .error-btn {
55
+ background: #dc3545;
56
+ color: white;
57
+ margin-left: 10px;
58
+ }
59
+
60
+ .articles-list {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 20px;
64
+ }
65
+
66
+ .article-item {
67
+ border: 1px solid #ddd;
68
+ padding: 20px;
69
+ border-radius: 8px;
70
+ background: #f9f9f9;
71
+ }
72
+
73
+ .article-title {
74
+ margin: 0 0 10px 0;
75
+ color: #333;
76
+ }
77
+
78
+ .article-content-preview {
79
+ margin: 0 0 15px 0;
80
+ color: #666;
81
+ }
82
+
83
+ .article-actions {
84
+ display: flex;
85
+ gap: 10px;
86
+ }
87
+
88
+ .article-detail {
89
+ background: white;
90
+ padding: 30px;
91
+ border-radius: 8px;
92
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
93
+ }
94
+
95
+ .article-detail-title {
96
+ margin: 0 0 20px 0;
97
+ color: #333;
98
+ border-bottom: 2px solid #007bff;
99
+ padding-bottom: 10px;
100
+ }
101
+
102
+ .article-detail-content {
103
+ line-height: 1.6;
104
+ color: #555;
105
+ white-space: pre-wrap;
106
+ }
107
+
108
+ .article-form {
109
+ background: white;
110
+ padding: 30px;
111
+ border-radius: 8px;
112
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
113
+ }
114
+
115
+ .form-group {
116
+ margin-bottom: 20px;
117
+ }
118
+
119
+ .form-group label {
120
+ display: block;
121
+ margin-bottom: 5px;
122
+ font-weight: bold;
123
+ color: #333;
124
+ }
125
+
126
+ .form-group input,
127
+ .form-group textarea {
128
+ width: 100%;
129
+ padding: 10px;
130
+ border: 1px solid #ddd;
131
+ border-radius: 4px;
132
+ font-size: 14px;
133
+ box-sizing: border-box;
134
+ }
135
+
136
+ .form-group textarea {
137
+ resize: vertical;
138
+ min-height: 120px;
139
+ }
140
+
141
+ .loading {
142
+ text-align: center;
143
+ padding: 40px;
144
+ color: #666;
145
+ }
146
+
147
+ .error {
148
+ background: #f8d7da;
149
+ color: #721c24;
150
+ padding: 15px;
151
+ border-radius: 4px;
152
+ margin-bottom: 20px;
153
+ display: flex;
154
+ justify-content: between;
155
+ align-items: center;
156
+ }
157
+
158
+ .no-articles {
159
+ text-align: center;
160
+ color: #666;
161
+ font-style: italic;
162
+ padding: 40px;
163
+ }
164
+
165
+ .app-footer {
166
+ margin-top: 40px;
167
+ padding-top: 20px;
168
+ border-top: 1px solid #ddd;
169
+ text-align: center;
170
+ color: #666;
171
+ }
@@ -0,0 +1,44 @@
1
+ module PaginationHelper
2
+ DEFAULT_PER_PAGE = 10
3
+ MAX_PER_PAGE = 100
4
+ MIN_PER_PAGE = 1
5
+
6
+ def self.extract_params(request, defaults: {})
7
+ defaults = { page: 1, per_page: DEFAULT_PER_PAGE }.merge(defaults)
8
+ query_string = request.env['QUERY_STRING'] || ''
9
+
10
+ page = (query_string.match(/[?&]page=(\d+)/) ? $1.to_i : nil) ||
11
+ request.params['page']&.to_i || defaults[:page]
12
+
13
+ per_page = (query_string.match(/[?&]per_page=(\d+)/) ? $1.to_i : nil) ||
14
+ request.params['per_page']&.to_i || defaults[:per_page]
15
+
16
+ page = [page, 1].max
17
+ per_page = [[per_page, MIN_PER_PAGE].max, MAX_PER_PAGE].min
18
+
19
+ { page: page, per_page: per_page }
20
+ end
21
+
22
+ def self.paginate(collection, page:, per_page:)
23
+ total_items = collection.size
24
+ total_pages = [(total_items.to_f / per_page).ceil, 1].max
25
+ page = [page, total_pages].min
26
+
27
+ start_index = (page - 1) * per_page
28
+ paginated_items = collection[start_index, per_page] || []
29
+
30
+ {
31
+ items: paginated_items,
32
+ pagination: {
33
+ current_page: page,
34
+ per_page: per_page,
35
+ total_items: total_items,
36
+ total_pages: total_pages,
37
+ has_previous: page > 1,
38
+ has_next: page < total_pages,
39
+ previous_page: page > 1 ? page - 1 : nil,
40
+ next_page: page < total_pages ? page + 1 : nil
41
+ }
42
+ }
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ # shared/helpers/response_formatter.rb
2
+ require 'json'
3
+
4
+ class ResponseFormatter
5
+ def self.json(data, status: 200)
6
+ # Convert data to JSON-serializable format
7
+ json_data = if data.is_a?(Array)
8
+ data.map { |item| item.respond_to?(:to_h) ? item.to_h : item }
9
+ elsif data.respond_to?(:to_h)
10
+ data.to_h
11
+ else
12
+ data
13
+ end
14
+
15
+ [status,
16
+ {'Content-Type' => 'application/json'},
17
+ [json_data.to_json]]
18
+ end
19
+
20
+ def self.html(content, status: 200)
21
+ [status,
22
+ {'Content-Type' => 'text/html; charset=utf-8'},
23
+ [content]]
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # test/acceptance/api/articles_api_acceptance_test.rb
2
+ require_relative "../../test_helper"
3
+
4
+ class ArticlesApiAcceptanceTest < Minitest::Test
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ # This should point to your actual API server
9
+ # You might need to create this based on your infra/server setup
10
+ ->(env) {
11
+ # Simple mock API for acceptance testing
12
+ case env['PATH_INFO']
13
+ when '/api/articles'
14
+ [200, { 'Content-Type' => 'application/json' }, ['[{"id":"1","title":"Test"}]']]
15
+ when '/api/articles/1'
16
+ [200, { 'Content-Type' => 'application/json' }, ['{"id":"1","title":"Test"}']]
17
+ else
18
+ [404, {}, ['Not Found']]
19
+ end
20
+ }
21
+ end
22
+
23
+ def test_api_articles_endpoint
24
+ get '/api/articles'
25
+
26
+ assert_equal 200, last_response.status
27
+ assert_equal 'application/json', last_response.content_type
28
+
29
+ response = JSON.parse(last_response.body)
30
+ assert response.is_a?(Array)
31
+ end
32
+
33
+ def test_api_single_article_endpoint
34
+ get '/api/articles/1'
35
+
36
+ assert_equal 200, last_response.status
37
+ assert_equal 'application/json', last_response.content_type
38
+
39
+ response = JSON.parse(last_response.body)
40
+ assert response["id"]
41
+ assert response["title"]
42
+ end
43
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../../test_helper"
2
+
3
+ class ArticlesWebAcceptanceTest < Minitest::Test
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ ->(env) {
8
+ [200, {'Content-Type' => 'text/html'}, ['Test response']]
9
+ }
10
+ end
11
+
12
+ def test_visit_articles_index
13
+ get '/articles'
14
+ assert_equal 200, last_response.status
15
+ end
16
+
17
+ def test_visit_article_show
18
+ get '/articles/1'
19
+ assert_equal 200, last_response.status
20
+ end
21
+
22
+ def test_visit_new_article_page
23
+ get '/articles/new'
24
+ assert_equal 200, last_response.status
25
+ end
26
+
27
+ def test_visit_edit_article_page
28
+ get '/articles/1/edit'
29
+ assert_equal 200, last_response.status
30
+ end
31
+ end