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,17 @@
1
+ require_relative "../../test_helper"
2
+
3
+ class HomeAcceptanceTest < Minitest::Test
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ ->(env) {
8
+ [200, {'Content-Type' => 'text/html'}, ['Home page']]
9
+ }
10
+ end
11
+
12
+ def test_visit_homepage
13
+ get '/'
14
+ assert_equal 200, last_response.status
15
+ assert_includes last_response.body, 'Home page'
16
+ end
17
+ end
@@ -0,0 +1,106 @@
1
+ # test/db.rb
2
+
3
+ require 'yaml'
4
+ require 'sequel'
5
+
6
+ # Load environment config
7
+ config = YAML.load_file('env.yml')
8
+ test_config = config['test'] # test /development / production
9
+
10
+ puts "Testing database connection with config:"
11
+ puts " Database: #{test_config['DB_DATABASE']}"
12
+ puts " Adapter: #{test_config['DB_CONNECTION']}"
13
+ puts " Host: #{test_config['DB_HOST']}"
14
+
15
+ begin
16
+ # Map adapter names for Sequel
17
+ adapter = case test_config['DB_CONNECTION'].to_s.downcase
18
+ when 'mysql', 'mysql2' then 'mysql2'
19
+ when 'postgres', 'postgresql', 'pgsql' then 'postgres'
20
+ when 'sqlite', 'sqlite3' then 'sqlite'
21
+ else
22
+ test_config['DB_CONNECTION']
23
+ end
24
+
25
+ # Build connection options based on adapter type
26
+ connection_options = {
27
+ adapter: adapter,
28
+ database: test_config['DB_DATABASE']
29
+ }
30
+
31
+ # Add common options for client-server databases
32
+ case adapter
33
+ when 'mysql2', 'postgres'
34
+ connection_options[:host] = test_config['DB_HOST'] if test_config['DB_HOST']
35
+ connection_options[:port] = test_config['DB_PORT'] if test_config['DB_PORT']
36
+ connection_options[:user] = test_config['DB_USERNAME'] if test_config['DB_USERNAME']
37
+ connection_options[:password] = test_config['DB_PASSWORD'] if test_config['DB_PASSWORD']
38
+ connection_options[:encoding] = test_config['DB_CHARSET'] if test_config['DB_CHARSET']
39
+
40
+ # MySQL-specific options
41
+ if adapter == 'mysql2'
42
+ # Only use socket if specified and file exists
43
+ if test_config['DB_SOCKET'] && File.exist?(test_config['DB_SOCKET'])
44
+ connection_options[:socket] = test_config['DB_SOCKET']
45
+ puts " Using socket: #{test_config['DB_SOCKET']}"
46
+ else
47
+ puts " Using TCP connection"
48
+ end
49
+ end
50
+
51
+ when 'sqlite'
52
+ # SQLite doesn't need host/port/user/password
53
+ puts " Using SQLite database file"
54
+ else
55
+ puts " Unknown adapter: #{adapter}, using basic connection"
56
+ end
57
+
58
+ puts " Connection options: #{connection_options.reject { |k,v| k == :password }}"
59
+
60
+ # Connect to the database
61
+ db = Sequel.connect(connection_options)
62
+
63
+ # Test connection
64
+ db.test_connection
65
+ puts "✅ Database connection successful!"
66
+
67
+ # Show tables
68
+ tables = db.tables
69
+ if tables.any?
70
+ puts "📊 Tables in database: #{tables.join(', ')}"
71
+ else
72
+ puts "📊 No tables in database"
73
+ end
74
+
75
+ # Show database version
76
+ begin
77
+ version = case adapter
78
+ when 'mysql2' then db['SELECT VERSION() as version'].first[:version]
79
+ when 'postgres' then db['SELECT version()'].first[:version].split(',')[0]
80
+ when 'sqlite' then db['SELECT sqlite_version()'].first.values.first
81
+ else "Unknown"
82
+ end
83
+ puts "🔧 Database version: #{version}"
84
+ rescue => e
85
+ puts "🔧 Could not determine database version: #{e.message}"
86
+ end
87
+
88
+ db.disconnect
89
+
90
+ rescue LoadError => e
91
+ puts "❌ Missing gem: #{e.message}"
92
+ puts "💡 Tip: Make sure the required database gem is installed:"
93
+ case adapter
94
+ when 'mysql2' then puts " gem install mysql2"
95
+ when 'postgres' then puts " gem install pg"
96
+ when 'sqlite' then puts " gem install sqlite3"
97
+ end
98
+ rescue => e
99
+ puts "❌ Database connection failed: #{e.message}"
100
+ puts "💡 Debug info:"
101
+ puts " - Adapter: #{adapter}"
102
+ puts " - Connection options used: #{connection_options.reject { |k,v| k == :password }.inspect}"
103
+ if test_config['DB_SOCKET']
104
+ puts " - Socket file exists: #{File.exist?(test_config['DB_SOCKET'])}"
105
+ end
106
+ end
@@ -0,0 +1,79 @@
1
+ # test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb
2
+
3
+ require_relative "./../../../test_helper"
4
+
5
+
6
+ class SequelArticlesGatewayTest < Minitest::Test
7
+ def setup
8
+ skip_if_no_database # This test requires a real database
9
+ @gateway = SequelArticlesGateway.new
10
+ clear_test_data
11
+ end
12
+
13
+ def teardown
14
+ clear_test_data if database_available?
15
+ end
16
+
17
+
18
+ def test_save_new_article
19
+ article = Article.new(title: "Test Article", content: "Test content")
20
+
21
+ saved_article = @gateway.save(article)
22
+
23
+ assert saved_article.id
24
+ assert_equal "Test Article", saved_article.title
25
+ assert_equal "Test content", saved_article.content
26
+ end
27
+
28
+ def test_find_article
29
+ article = Article.new(title: "Find Me", content: "Content")
30
+ saved_article = @gateway.save(article)
31
+
32
+ found_article = @gateway.find(saved_article.id)
33
+
34
+ assert_equal saved_article.id, found_article.id
35
+ assert_equal "Find Me", found_article.title
36
+ end
37
+
38
+ def test_list_articles
39
+ @gateway.save(Article.new(title: "First", content: "Content1"))
40
+ @gateway.save(Article.new(title: "Second", content: "Content2"))
41
+
42
+ articles = @gateway.all
43
+
44
+ assert_equal 2, articles.size
45
+ # Remove the order assertion or fix the expected order
46
+ # Just test that both articles are returned
47
+ titles = articles.map(&:title)
48
+ assert_includes titles, "First"
49
+ assert_includes titles, "Second"
50
+ end
51
+
52
+ def test_update_article
53
+ article = Article.new(title: "Original", content: "Content")
54
+ saved_article = @gateway.save(article)
55
+
56
+ saved_article.title = "Updated"
57
+ updated_article = @gateway.save(saved_article)
58
+
59
+ assert_equal "Updated", updated_article.title
60
+ assert_equal saved_article.id, updated_article.id
61
+ end
62
+
63
+ def test_delete_article
64
+ article = Article.new(title: "To Delete", content: "Content")
65
+ saved_article = @gateway.save(article)
66
+
67
+ @gateway.delete(saved_article.id)
68
+
69
+ assert_nil @gateway.find(saved_article.id)
70
+ end
71
+
72
+ private
73
+
74
+ def clear_test_data
75
+ # This should be defined in your test_helper.rb
76
+ db = SequelConnection.db
77
+ db[:articles].delete if db.tables.include?(:articles)
78
+ end
79
+ end
@@ -0,0 +1,61 @@
1
+ # In test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb
2
+
3
+ def test_create_article
4
+ post '/api/articles',
5
+ { title: "New API Article", content: "API Content" }.to_json,
6
+ { 'CONTENT_TYPE' => 'application/json' }
7
+
8
+ assert_equal 201, last_response.status
9
+ response = JSON.parse(last_response.body)
10
+
11
+ # Debug the actual response
12
+ puts "DEBUG: Create response: #{response}"
13
+
14
+ assert response["id"], "Article should have an ID. Response: #{response}"
15
+ assert_equal "New API Article", response["title"]
16
+ assert_equal "API Content", response["content"]
17
+ end
18
+
19
+ def test_get_article
20
+ # Create test article through the API first
21
+ post '/api/articles',
22
+ { title: "Single Article", content: "Single Content" }.to_json,
23
+ { 'CONTENT_TYPE' => 'application/json' }
24
+
25
+ assert_equal 201, last_response.status
26
+ created_article = JSON.parse(last_response.body)
27
+ puts "DEBUG: Created article: #{created_article}"
28
+
29
+ get "/api/articles/#{created_article['id']}"
30
+
31
+ puts "DEBUG: Get response status: #{last_response.status}"
32
+ puts "DEBUG: Get response body: #{last_response.body}"
33
+
34
+ assert_equal 200, last_response.status
35
+ response = JSON.parse(last_response.body)
36
+ assert_equal "Single Article", response["title"]
37
+ assert_equal "Single Content", response["content"]
38
+ end
39
+
40
+ def test_update_article
41
+ # Create article first through the API
42
+ post '/api/articles',
43
+ { title: "Original", content: "Original Content" }.to_json,
44
+ { 'CONTENT_TYPE' => 'application/json' }
45
+
46
+ assert_equal 201, last_response.status
47
+ created_article = JSON.parse(last_response.body)
48
+ puts "DEBUG: Created article for update: #{created_article}"
49
+
50
+ put "/api/articles/#{created_article['id']}",
51
+ { title: "Updated", content: "Updated Content" }.to_json,
52
+ { 'CONTENT_TYPE' => 'application/json' }
53
+
54
+ puts "DEBUG: Update response status: #{last_response.status}"
55
+ puts "DEBUG: Update response body: #{last_response.body}"
56
+
57
+ assert_equal 200, last_response.status
58
+ response = JSON.parse(last_response.body)
59
+ assert_equal "Updated", response["title"]
60
+ assert_equal "Updated Content", response["content"]
61
+ end
@@ -0,0 +1,20 @@
1
+ # test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb
2
+ require_relative "../../../../test_helper"
3
+
4
+ class ArticlesWebAdapterTest < Minitest::Test
5
+ def setup
6
+ @adapter = ArticlesWebAdapter.new
7
+ end
8
+
9
+ def test_index_action
10
+ request = mock('request')
11
+ request.stubs(:params).returns({})
12
+
13
+ response = @adapter.index(request)
14
+
15
+ assert_equal 200, response[:status]
16
+ assert_equal 'pages/articles/index.hyr', response[:display]
17
+ assert response[:locals][:articles]
18
+ assert_kind_of Array, response[:locals][:articles]
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "../../../../test_helper"
2
+
3
+ class HomeWebAdapterTest < Minitest::Test
4
+ def setup
5
+ @adapter = HomeWebAdapter.new
6
+ end
7
+
8
+ def test_home_page
9
+ request = mock('request')
10
+
11
+ response = @adapter.home_page(request)
12
+
13
+ assert_equal 200, response[:status]
14
+ assert_equal 'home/home.hyr', response[:display]
15
+ assert_equal "Welcome to Hyraft", response[:locals][:page_title]
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ # test/integration/database/migration_test.rb
2
+ require_relative "../../test_helper"
3
+
4
+ class MigrationTest < Minitest::Test
5
+ def setup
6
+ skip_if_no_database
7
+ end
8
+
9
+ def test_migrations_can_run_successfully
10
+ # Remove the duplicate skip condition - setup already handles it
11
+ db = SequelConnection.db
12
+ assert db, "Database connection should be established"
13
+
14
+ assert db.tables.include?(:articles), "Articles table should exist from migrations"
15
+
16
+ columns = db.schema(:articles).map { |col| col.first }
17
+
18
+ assert_includes columns, :id
19
+ assert_includes columns, :title
20
+ assert_includes columns, :content
21
+ end
22
+
23
+ def test_database_connection_works_for_migrations
24
+ db = SequelConnection.db
25
+ assert db.test_connection
26
+ end
27
+
28
+ private
29
+
30
+ def skip_if_no_database
31
+ if ENV['TEST_NO_DB'] == '1'
32
+ skip "Skipping database test (TEST_NO_DB=1)"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,82 @@
1
+ # test/support/mock_api_adapter.rb
2
+ module MockApiAdapter
3
+ def initialize
4
+ @gateway = MockArticlesGateway.new
5
+ @articles_circuit = ArticlesCircuit.new(@gateway)
6
+ end
7
+
8
+ def index(env)
9
+ begin
10
+ articles = @articles_circuit.list
11
+ [200, { 'Content-Type' => 'application/json' }, [articles.to_json]]
12
+ rescue => e
13
+ puts "API Error: #{e.message}"
14
+ [500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
15
+ end
16
+ end
17
+
18
+ def show(env, id)
19
+ begin
20
+ article = @articles_circuit.find(id)
21
+ if article
22
+ [200, { 'Content-Type' => 'application/json' }, [article.to_json]]
23
+ else
24
+ [404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
25
+ end
26
+ rescue => e
27
+ puts "API Error: #{e.message}"
28
+ [500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
29
+ end
30
+ end
31
+
32
+ def create(env)
33
+ request = Rack::Request.new(env)
34
+ begin
35
+ data = JSON.parse(request.body.read)
36
+ article_data = data.transform_keys(&:to_sym)
37
+ article = @articles_circuit.create(**article_data)
38
+ if article
39
+ [201, { 'Content-Type' => 'application/json' }, [article.to_json]]
40
+ else
41
+ [422, { 'Content-Type' => 'application/json' }, [{ error: "Title and content are required" }.to_json]]
42
+ end
43
+ rescue JSON::ParserError
44
+ [400, { 'Content-Type' => 'application/json' }, [{ error: "Invalid JSON" }.to_json]]
45
+ rescue => e
46
+ puts "API Error: #{e.message}"
47
+ [422, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
48
+ end
49
+ end
50
+
51
+ def update(env, id)
52
+ request = Rack::Request.new(env)
53
+ begin
54
+ data = JSON.parse(request.body.read)
55
+ article = @articles_circuit.update(id: id, **data.transform_keys(&:to_sym))
56
+ if article
57
+ [200, { 'Content-Type' => 'application/json' }, [article.to_json]]
58
+ else
59
+ [404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
60
+ end
61
+ rescue JSON::ParserError
62
+ [400, { 'Content-Type' => 'application/json' }, [{ error: "Invalid JSON" }.to_json]]
63
+ rescue => e
64
+ puts "API Update Error: #{e.message}"
65
+ [422, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
66
+ end
67
+ end
68
+
69
+ def delete(env, id)
70
+ begin
71
+ result = @articles_circuit.delete(id)
72
+ if result
73
+ [200, { 'Content-Type' => 'application/json' }, [{ message: "Article deleted successfully" }.to_json]]
74
+ else
75
+ [404, { 'Content-Type' => 'application/json' }, [{ error: "Article not found" }.to_json]]
76
+ end
77
+ rescue => e
78
+ puts "API Error: #{e.message}"
79
+ [500, { 'Content-Type' => 'application/json' }, [{ error: "Internal server error" }.to_json]]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,41 @@
1
+ # test/support/mock_articles_gateway.rb
2
+ class MockArticlesGateway
3
+ def initialize
4
+ @storage = {}
5
+ @next_id = 1
6
+ end
7
+
8
+ def all
9
+ @storage.values.sort_by { |a| a.id.to_i }
10
+ end
11
+
12
+ def find(id)
13
+ @storage[id.to_s]
14
+ end
15
+
16
+ def save(article)
17
+ # Ensure the article has proper timestamps
18
+ current_time = Time.now
19
+ if article.created_at.nil?
20
+ article.created_at = current_time
21
+ end
22
+ article.updated_at = current_time
23
+
24
+ if article.id.nil?
25
+ article.id = @next_id.to_s
26
+ @next_id += 1
27
+ end
28
+
29
+ @storage[article.id] = article
30
+ article
31
+ end
32
+
33
+ def delete(id)
34
+ !!@storage.delete(id.to_s)
35
+ end
36
+
37
+ def clear
38
+ @storage.clear
39
+ @next_id = 1
40
+ end
41
+ end
@@ -0,0 +1,85 @@
1
+ # test/support/mock_web_adapter.rb
2
+ module MockWebAdapter
3
+ def initialize
4
+ @gateway = MockArticlesGateway.new
5
+ @articles_circuit = ArticlesCircuit.new(@gateway)
6
+ end
7
+
8
+ def index(request)
9
+ begin
10
+ articles = @articles_circuit.list
11
+ {
12
+ status: 200,
13
+ display: 'pages/articles/index.hyr',
14
+ locals: { articles: articles }
15
+ }
16
+ rescue => e
17
+ puts "Web Adapter Error: #{e.message}"
18
+ {
19
+ status: 500,
20
+ display: 'pages/error.hyr',
21
+ locals: { error: "Internal server error" }
22
+ }
23
+ end
24
+ end
25
+
26
+ def show(request, id)
27
+ begin
28
+ article = @articles_circuit.find(id)
29
+ if article
30
+ {
31
+ status: 200,
32
+ display: 'pages/articles/show.hyr',
33
+ locals: { article: article }
34
+ }
35
+ else
36
+ {
37
+ status: 404,
38
+ display: 'pages/error.hyr',
39
+ locals: { error: "Article not found" }
40
+ }
41
+ end
42
+ rescue => e
43
+ puts "Web Adapter Error: #{e.message}"
44
+ {
45
+ status: 500,
46
+ display: 'pages/error.hyr',
47
+ locals: { error: "Internal server error" }
48
+ }
49
+ end
50
+ end
51
+
52
+ def new(request)
53
+ {
54
+ status: 200,
55
+ display: 'pages/articles/new.hyr',
56
+ locals: { article: Article.new }
57
+ }
58
+ end
59
+
60
+ def edit(request, id)
61
+ begin
62
+ article = @articles_circuit.find(id)
63
+ if article
64
+ {
65
+ status: 200,
66
+ display: 'pages/articles/edit.hyr',
67
+ locals: { article: article }
68
+ }
69
+ else
70
+ {
71
+ status: 404,
72
+ display: 'pages/error.hyr',
73
+ locals: { error: "Article not found" }
74
+ }
75
+ end
76
+ rescue => e
77
+ puts "Web Adapter Error: #{e.message}"
78
+ {
79
+ status: 500,
80
+ display: 'pages/error.hyr',
81
+ locals: { error: "Internal server error" }
82
+ }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ # test/support/test_patches.rb
2
+ puts "Applying test patches for TEST_NO_DB=1..."
3
+
4
+ # Patch Article for proper JSON serialization
5
+ class Article
6
+ def to_json(*args)
7
+ to_hash.to_json(*args)
8
+ end
9
+
10
+ def to_hash
11
+ {
12
+ id: id,
13
+ title: title,
14
+ content: content,
15
+ status: status,
16
+ created_at: created_at,
17
+ updated_at: updated_at
18
+ }.compact
19
+ end
20
+ end
21
+
22
+ # Redefine adapters to use MockArticlesGateway
23
+ if defined?(ArticlesApiAdapter)
24
+ require_relative 'mock_api_adapter'
25
+ ArticlesApiAdapter.prepend(MockApiAdapter)
26
+ puts "✓ Patched ArticlesApiAdapter"
27
+ end
28
+
29
+ if defined?(ArticlesWebAdapter)
30
+ require_relative 'mock_web_adapter'
31
+ ArticlesWebAdapter.prepend(MockWebAdapter)
32
+ puts "✓ Patched ArticlesWebAdapter"
33
+ end