tokra 0.0.1.pre.1 → 0.0.1.pre.2

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSES/MIT-0.txt +16 -0
  3. data/REUSE.toml +6 -1
  4. data/Rakefile +2 -0
  5. data/Steepfile +2 -0
  6. data/clippy_exceptions.rb +75 -37
  7. data/doc/contributors/adr/004.md +63 -0
  8. data/doc/contributors/chats/002.md +177 -0
  9. data/doc/contributors/chats/003.md +2180 -0
  10. data/doc/contributors/chats/004.md +1992 -0
  11. data/doc/contributors/chats/005.md +1529 -0
  12. data/doc/contributors/plan/002.md +173 -0
  13. data/doc/contributors/plan/003.md +111 -0
  14. data/examples/verify_hello_world/index.html +15 -2
  15. data/examples/verify_ping_pong/app.rb +3 -1
  16. data/examples/verify_ping_pong/public/styles.css +36 -9
  17. data/examples/verify_ping_pong/views/layout.erb +1 -1
  18. data/examples/verify_rails_sqlite/.dockerignore +51 -0
  19. data/examples/verify_rails_sqlite/.gitattributes +9 -0
  20. data/examples/verify_rails_sqlite/.github/dependabot.yml +12 -0
  21. data/examples/verify_rails_sqlite/.github/workflows/ci.yml +124 -0
  22. data/examples/verify_rails_sqlite/.gitignore +35 -0
  23. data/examples/verify_rails_sqlite/.kamal/hooks/docker-setup.sample +3 -0
  24. data/examples/verify_rails_sqlite/.kamal/hooks/post-app-boot.sample +3 -0
  25. data/examples/verify_rails_sqlite/.kamal/hooks/post-deploy.sample +14 -0
  26. data/examples/verify_rails_sqlite/.kamal/hooks/post-proxy-reboot.sample +3 -0
  27. data/examples/verify_rails_sqlite/.kamal/hooks/pre-app-boot.sample +3 -0
  28. data/examples/verify_rails_sqlite/.kamal/hooks/pre-build.sample +51 -0
  29. data/examples/verify_rails_sqlite/.kamal/hooks/pre-connect.sample +47 -0
  30. data/examples/verify_rails_sqlite/.kamal/hooks/pre-deploy.sample +122 -0
  31. data/examples/verify_rails_sqlite/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  32. data/examples/verify_rails_sqlite/.kamal/secrets +20 -0
  33. data/examples/verify_rails_sqlite/.rubocop.yml +2 -0
  34. data/examples/verify_rails_sqlite/.ruby-version +1 -0
  35. data/examples/verify_rails_sqlite/Dockerfile +77 -0
  36. data/examples/verify_rails_sqlite/Gemfile +66 -0
  37. data/examples/verify_rails_sqlite/Gemfile.lock +563 -0
  38. data/examples/verify_rails_sqlite/README.md +41 -0
  39. data/examples/verify_rails_sqlite/Rakefile +6 -0
  40. data/examples/verify_rails_sqlite/app/assets/images/.keep +0 -0
  41. data/examples/verify_rails_sqlite/app/assets/stylesheets/application.css +469 -0
  42. data/examples/verify_rails_sqlite/app/controllers/application_controller.rb +12 -0
  43. data/examples/verify_rails_sqlite/app/controllers/concerns/.keep +0 -0
  44. data/examples/verify_rails_sqlite/app/controllers/todos_controller.rb +70 -0
  45. data/examples/verify_rails_sqlite/app/helpers/application_helper.rb +2 -0
  46. data/examples/verify_rails_sqlite/app/helpers/todos_helper.rb +2 -0
  47. data/examples/verify_rails_sqlite/app/javascript/application.js +3 -0
  48. data/examples/verify_rails_sqlite/app/javascript/controllers/application.js +9 -0
  49. data/examples/verify_rails_sqlite/app/javascript/controllers/hello_controller.js +7 -0
  50. data/examples/verify_rails_sqlite/app/javascript/controllers/index.js +4 -0
  51. data/examples/verify_rails_sqlite/app/jobs/application_job.rb +7 -0
  52. data/examples/verify_rails_sqlite/app/mailers/application_mailer.rb +4 -0
  53. data/examples/verify_rails_sqlite/app/models/application_record.rb +3 -0
  54. data/examples/verify_rails_sqlite/app/models/concerns/.keep +0 -0
  55. data/examples/verify_rails_sqlite/app/models/todo.rb +10 -0
  56. data/examples/verify_rails_sqlite/app/views/layouts/application.html.erb +24 -0
  57. data/examples/verify_rails_sqlite/app/views/layouts/mailer.html.erb +13 -0
  58. data/examples/verify_rails_sqlite/app/views/layouts/mailer.text.erb +1 -0
  59. data/examples/verify_rails_sqlite/app/views/pwa/manifest.json.erb +22 -0
  60. data/examples/verify_rails_sqlite/app/views/pwa/service-worker.js +26 -0
  61. data/examples/verify_rails_sqlite/app/views/todos/_form.html.erb +27 -0
  62. data/examples/verify_rails_sqlite/app/views/todos/_todo.html.erb +18 -0
  63. data/examples/verify_rails_sqlite/app/views/todos/_todo.json.jbuilder +2 -0
  64. data/examples/verify_rails_sqlite/app/views/todos/edit.html.erb +7 -0
  65. data/examples/verify_rails_sqlite/app/views/todos/index.html.erb +22 -0
  66. data/examples/verify_rails_sqlite/app/views/todos/index.json.jbuilder +1 -0
  67. data/examples/verify_rails_sqlite/app/views/todos/new.html.erb +7 -0
  68. data/examples/verify_rails_sqlite/app/views/todos/show.html.erb +23 -0
  69. data/examples/verify_rails_sqlite/app/views/todos/show.json.jbuilder +1 -0
  70. data/examples/verify_rails_sqlite/bin/brakeman +7 -0
  71. data/examples/verify_rails_sqlite/bin/bundler-audit +6 -0
  72. data/examples/verify_rails_sqlite/bin/ci +6 -0
  73. data/examples/verify_rails_sqlite/bin/dev +2 -0
  74. data/examples/verify_rails_sqlite/bin/docker-entrypoint +8 -0
  75. data/examples/verify_rails_sqlite/bin/importmap +4 -0
  76. data/examples/verify_rails_sqlite/bin/jobs +6 -0
  77. data/examples/verify_rails_sqlite/bin/kamal +16 -0
  78. data/examples/verify_rails_sqlite/bin/rails +4 -0
  79. data/examples/verify_rails_sqlite/bin/rake +4 -0
  80. data/examples/verify_rails_sqlite/bin/rubocop +8 -0
  81. data/examples/verify_rails_sqlite/bin/setup +35 -0
  82. data/examples/verify_rails_sqlite/bin/thrust +5 -0
  83. data/examples/verify_rails_sqlite/config/application.rb +27 -0
  84. data/examples/verify_rails_sqlite/config/boot.rb +4 -0
  85. data/examples/verify_rails_sqlite/config/bundler-audit.yml +5 -0
  86. data/examples/verify_rails_sqlite/config/cable.yml +17 -0
  87. data/examples/verify_rails_sqlite/config/cache.yml +16 -0
  88. data/examples/verify_rails_sqlite/config/ci.rb +24 -0
  89. data/examples/verify_rails_sqlite/config/credentials.yml.enc +1 -0
  90. data/examples/verify_rails_sqlite/config/database.yml +40 -0
  91. data/examples/verify_rails_sqlite/config/deploy.yml +119 -0
  92. data/examples/verify_rails_sqlite/config/environment.rb +5 -0
  93. data/examples/verify_rails_sqlite/config/environments/development.rb +84 -0
  94. data/examples/verify_rails_sqlite/config/environments/production.rb +99 -0
  95. data/examples/verify_rails_sqlite/config/environments/test.rb +53 -0
  96. data/examples/verify_rails_sqlite/config/importmap.rb +7 -0
  97. data/examples/verify_rails_sqlite/config/initializers/assets.rb +7 -0
  98. data/examples/verify_rails_sqlite/config/initializers/content_security_policy.rb +29 -0
  99. data/examples/verify_rails_sqlite/config/initializers/filter_parameter_logging.rb +8 -0
  100. data/examples/verify_rails_sqlite/config/initializers/inflections.rb +16 -0
  101. data/examples/verify_rails_sqlite/config/locales/en.yml +31 -0
  102. data/examples/verify_rails_sqlite/config/puma.rb +42 -0
  103. data/examples/verify_rails_sqlite/config/queue.yml +18 -0
  104. data/examples/verify_rails_sqlite/config/recurring.yml +15 -0
  105. data/examples/verify_rails_sqlite/config/routes.rb +15 -0
  106. data/examples/verify_rails_sqlite/config/storage.yml +27 -0
  107. data/examples/verify_rails_sqlite/config.ru +6 -0
  108. data/examples/verify_rails_sqlite/db/cable_schema.rb +11 -0
  109. data/examples/verify_rails_sqlite/db/cache_schema.rb +12 -0
  110. data/examples/verify_rails_sqlite/db/migrate/20260130080933_create_todos.rb +10 -0
  111. data/examples/verify_rails_sqlite/db/queue_schema.rb +129 -0
  112. data/examples/verify_rails_sqlite/db/schema.rb +20 -0
  113. data/examples/verify_rails_sqlite/db/seeds.rb +9 -0
  114. data/examples/verify_rails_sqlite/lib/tasks/.keep +0 -0
  115. data/examples/verify_rails_sqlite/log/.keep +0 -0
  116. data/examples/verify_rails_sqlite/public/400.html +135 -0
  117. data/examples/verify_rails_sqlite/public/404.html +135 -0
  118. data/examples/verify_rails_sqlite/public/406-unsupported-browser.html +135 -0
  119. data/examples/verify_rails_sqlite/public/422.html +135 -0
  120. data/examples/verify_rails_sqlite/public/500.html +135 -0
  121. data/examples/verify_rails_sqlite/public/icon.png +0 -0
  122. data/examples/verify_rails_sqlite/public/icon.svg +3 -0
  123. data/examples/verify_rails_sqlite/public/robots.txt +1 -0
  124. data/examples/verify_rails_sqlite/public/styles.css +469 -0
  125. data/examples/verify_rails_sqlite/script/.keep +0 -0
  126. data/examples/verify_rails_sqlite/storage/.keep +0 -0
  127. data/examples/verify_rails_sqlite/test/controllers/.keep +0 -0
  128. data/examples/verify_rails_sqlite/test/controllers/todos_controller_test.rb +48 -0
  129. data/examples/verify_rails_sqlite/test/fixtures/files/.keep +0 -0
  130. data/examples/verify_rails_sqlite/test/fixtures/todos.yml +9 -0
  131. data/examples/verify_rails_sqlite/test/helpers/.keep +0 -0
  132. data/examples/verify_rails_sqlite/test/integration/.keep +0 -0
  133. data/examples/verify_rails_sqlite/test/mailers/.keep +0 -0
  134. data/examples/verify_rails_sqlite/test/models/.keep +0 -0
  135. data/examples/verify_rails_sqlite/test/models/todo_test.rb +7 -0
  136. data/examples/verify_rails_sqlite/test/test_helper.rb +15 -0
  137. data/examples/verify_rails_sqlite/tmp/.keep +0 -0
  138. data/examples/verify_rails_sqlite/tmp/pids/.keep +0 -0
  139. data/examples/verify_rails_sqlite/tmp/storage/.keep +0 -0
  140. data/examples/verify_rails_sqlite/tokra.rb +42 -0
  141. data/examples/verify_rails_sqlite/vendor/.keep +0 -0
  142. data/examples/verify_rails_sqlite/vendor/javascript/.keep +0 -0
  143. data/ext/tokra/src/event_loop.rs +206 -0
  144. data/ext/tokra/src/events.rs +430 -0
  145. data/ext/tokra/src/lib.rs +52 -664
  146. data/ext/tokra/src/proxy.rs +142 -0
  147. data/ext/tokra/src/responders.rs +86 -0
  148. data/ext/tokra/src/user_event.rs +313 -0
  149. data/ext/tokra/src/webview.rs +194 -0
  150. data/ext/tokra/src/window.rs +92 -0
  151. data/lib/tokra/rack/handler.rb +37 -14
  152. data/lib/tokra/version.rb +1 -1
  153. data/rbs_exceptions.rb +12 -0
  154. data/sig/tokra.rbs +95 -1
  155. data/tasks/lint.rake +2 -2
  156. data/tasks/lint.rb +49 -0
  157. data/tasks/rust.rake +6 -23
  158. data/tasks/steep.rake +25 -3
  159. data/tasks/test.rake +1 -1
  160. metadata +143 -1
@@ -0,0 +1,15 @@
1
+ ENV["RAILS_ENV"] ||= "test"
2
+ require_relative "../config/environment"
3
+ require "rails/test_help"
4
+
5
+ module ActiveSupport
6
+ class TestCase
7
+ # Run tests in parallel with specified workers
8
+ parallelize(workers: :number_of_processors)
9
+
10
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
11
+ fixtures :all
12
+
13
+ # Add more helper methods to be used by all tests here...
14
+ end
15
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ #!/usr/bin/env ruby
9
+ # Tokra Rails Runner - Direct, no middleware
10
+
11
+ require_relative "../../lib/tokra"
12
+
13
+ ENV["RAILS_ENV"] = "production"
14
+ require_relative "config/environment"
15
+
16
+ require "logger"
17
+
18
+ LOGGER = Logger.new($stdout)
19
+ LOGGER.level = Logger::INFO
20
+ LOGGER.progname = "TokraRails"
21
+ LOGGER.formatter = proc do |severity, datetime, progname, msg|
22
+ timestamp = datetime.strftime("%H:%M:%S.%L")
23
+ "#{timestamp} [#{severity.ljust(5)}] #{progname}: #{msg}\n"
24
+ end
25
+
26
+ LOGGER.info { "=" * 60 }
27
+ LOGGER.info { "Tokra Rails - Direct (No Middleware)" }
28
+ LOGGER.info { "=" * 60 }
29
+ LOGGER.info { "Ruby #{RUBY_VERSION} | Rails #{Rails::VERSION::STRING} | Tokra #{Tokra::VERSION}" }
30
+ LOGGER.info { "Starting Rails application..." }
31
+
32
+ Rack::Handler::Tokra.run(
33
+ Rails.application,
34
+ title: "Tokra Tasks",
35
+ width: 900.0,
36
+ height: 700.0,
37
+ invoke_handler: proc { |msg|
38
+ LOGGER.info { "IPC: #{msg}" }
39
+ }
40
+ )
41
+
42
+ LOGGER.info { "Goodbye!" }
File without changes
@@ -0,0 +1,206 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Event loop wrapper for tao.
5
+ //!
6
+ //! This module provides the Ruby-facing `Tokra::Native::EventLoop` class,
7
+ //! which wraps `tao::event_loop::EventLoop<UserEvent>`.
8
+
9
+ use std::cell::RefCell;
10
+ use std::sync::atomic::{AtomicBool, Ordering};
11
+ use std::sync::Arc;
12
+
13
+ use magnus::{function, method, prelude::*, Error, Ruby, Value};
14
+ use tao::{
15
+ event::{Event, WindowEvent},
16
+ event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy},
17
+ };
18
+ use wry::http;
19
+
20
+ use crate::events::{
21
+ RbHttpRequestEvent, RbIpcEvent, RbPageLoadEvent, RbWakeUpEvent, RbWindowCloseEvent,
22
+ };
23
+ use crate::proxy::RbProxy;
24
+ use crate::responders::get_responders;
25
+ use crate::runtime_error;
26
+ use crate::user_event::UserEvent;
27
+
28
+ /// Ruby wrapper for `tao::event_loop::EventLoop<UserEvent>`.
29
+ ///
30
+ /// The `EventLoop` owns the OS main thread and runs the window event pump.
31
+ #[magnus::wrap(class = "Tokra::Native::EventLoop", free_immediately, size)]
32
+ pub struct RbEventLoop {
33
+ /// The inner event loop. Wrapped in Option because `run` consumes it.
34
+ pub(crate) inner: RefCell<Option<EventLoop<UserEvent>>>,
35
+ /// Proxy for creating `RbProxy` instances before `run()` consumes the loop.
36
+ pub(crate) proxy: RefCell<Option<EventLoopProxy<UserEvent>>>,
37
+ }
38
+
39
+ // SPDX-SnippetBegin
40
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
41
+ // SPDX-SnippetCopyrightText: 2020-2023 Tauri Programme within The Commons Conservancy
42
+ // SPDX-SnippetComment: Thread safety pattern adapted from tauri-runtime-wry/lib.rs unsafe impl Send for WindowsStore/DispatcherMainThreadContext
43
+
44
+ // SAFETY: RbEventLoop is only accessed from the main thread. The event loop
45
+ // itself runs on the main thread and all Ruby interactions happen there.
46
+ // This matches Tauri's pattern in tauri-runtime-wry.
47
+ // See clippy_exceptions.rb for full justification.
48
+ #[allow(unsafe_code)]
49
+ #[allow(clippy::non_send_fields_in_send_ty)]
50
+ unsafe impl Send for RbEventLoop {}
51
+
52
+ // SPDX-SnippetEnd
53
+
54
+ impl RbEventLoop {
55
+ /// Create a new `EventLoop`.
56
+ ///
57
+ /// # Errors
58
+ ///
59
+ /// This function is infallible but returns `Result` for consistency with Magnus FFI.
60
+ pub fn new() -> Result<Self, Error> {
61
+ let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
62
+
63
+ let proxy = event_loop.create_proxy();
64
+
65
+ Ok(Self {
66
+ inner: RefCell::new(Some(event_loop)),
67
+ proxy: RefCell::new(Some(proxy)),
68
+ })
69
+ }
70
+
71
+ /// Create a Proxy for cross-Ractor communication.
72
+ ///
73
+ /// Must be called before `run()` since run consumes the event loop.
74
+ ///
75
+ /// # Errors
76
+ ///
77
+ /// Returns an error if `run()` has already been called, consuming the event loop.
78
+ pub fn create_proxy(&self) -> Result<RbProxy, Error> {
79
+ let proxy = self.proxy.borrow().clone().ok_or_else(|| {
80
+ Error::new(
81
+ runtime_error(),
82
+ "EventLoop has already been consumed by run()",
83
+ )
84
+ })?;
85
+
86
+ Ok(RbProxy::new(proxy))
87
+ }
88
+
89
+ /// Run the event loop (blocking, never returns under normal operation).
90
+ ///
91
+ /// The callback Proc is invoked for each event.
92
+ ///
93
+ /// # Errors
94
+ ///
95
+ /// Returns an error if `run()` has already been called, consuming the event loop.
96
+ ///
97
+ /// # Panics
98
+ ///
99
+ /// Panics if the responders mutex is poisoned (internal invariant violation).
100
+ pub fn run(&self, callback: Value) -> Result<(), Error> {
101
+ let event_loop = self.inner.borrow_mut().take().ok_or_else(|| {
102
+ Error::new(
103
+ runtime_error(),
104
+ "EventLoop has already been consumed by run()",
105
+ )
106
+ })?;
107
+
108
+ // Setup ctrlc handler to send Exit event
109
+ let proxy_for_ctrlc = self.proxy.borrow().clone();
110
+ if let Some(proxy) = proxy_for_ctrlc {
111
+ let exit_sent = Arc::new(AtomicBool::new(false));
112
+
113
+ ctrlc::set_handler(move || {
114
+ if !exit_sent.swap(true, Ordering::SeqCst) {
115
+ let _ = proxy.send_event(UserEvent::Exit);
116
+ }
117
+ })
118
+ .ok(); // Ignore if already set
119
+ }
120
+
121
+ // Run the event loop - this is a blocking call
122
+ event_loop.run(move |event, _, control_flow| {
123
+ *control_flow = ControlFlow::Wait;
124
+
125
+ match event {
126
+ Event::UserEvent(user_event) => match user_event {
127
+ UserEvent::IpcMessage(msg) => {
128
+ let event_obj = RbIpcEvent::new(msg);
129
+ let _ = callback.funcall::<_, _, Value>("call", (event_obj,));
130
+ }
131
+ UserEvent::HttpRequest {
132
+ request_id,
133
+ method,
134
+ uri,
135
+ headers,
136
+ body,
137
+ } => {
138
+ let event_obj =
139
+ RbHttpRequestEvent::new(request_id, method, uri, headers, body);
140
+ let _ = callback.funcall::<_, _, Value>("call", (event_obj,));
141
+ }
142
+ UserEvent::HttpResponse {
143
+ request_id,
144
+ status,
145
+ headers,
146
+ body,
147
+ } => {
148
+ // Extract responder outside of if-let to avoid Drop-in-scrutinee
149
+ let responder = get_responders().lock().unwrap().remove(&request_id);
150
+ if let Some(responder) = responder {
151
+ let mut response_builder = http::Response::builder().status(status);
152
+ for (name, value) in &headers {
153
+ response_builder = response_builder.header(name, value);
154
+ }
155
+ let response =
156
+ response_builder
157
+ .body(body.into_bytes())
158
+ .unwrap_or_else(|_| {
159
+ http::Response::builder()
160
+ .status(500)
161
+ .body(b"Internal Error".to_vec())
162
+ .unwrap()
163
+ });
164
+ responder.respond(response);
165
+ }
166
+ }
167
+ UserEvent::PageLoad { event, url } => {
168
+ let event_obj = RbPageLoadEvent::new(event, url);
169
+ let _ = callback.funcall::<_, _, Value>("call", (event_obj,));
170
+ }
171
+ UserEvent::WakeUp(payload) => {
172
+ // Create a WakeUpEvent and call the Ruby callback
173
+ let event_obj = RbWakeUpEvent::new(payload);
174
+ let _ = callback.funcall::<_, _, Value>("call", (event_obj,));
175
+ }
176
+ UserEvent::Exit => {
177
+ *control_flow = ControlFlow::Exit;
178
+ }
179
+ },
180
+ Event::WindowEvent {
181
+ event: WindowEvent::CloseRequested,
182
+ ..
183
+ } => {
184
+ // Create a WindowCloseEvent and call the Ruby callback
185
+ let event_obj = RbWindowCloseEvent::new();
186
+ let _ = callback.funcall::<_, _, Value>("call", (event_obj,));
187
+ *control_flow = ControlFlow::Exit;
188
+ }
189
+ _ => {}
190
+ }
191
+ });
192
+ }
193
+ }
194
+
195
+ /// Define the `EventLoop` class methods on the given Ruby module.
196
+ ///
197
+ /// # Errors
198
+ ///
199
+ /// Returns an error if class or method definition fails.
200
+ pub fn define_class(ruby: &Ruby, native: magnus::RModule) -> Result<(), Error> {
201
+ let event_loop_class = native.define_class("EventLoop", ruby.class_object())?;
202
+ event_loop_class.define_singleton_method("new", function!(RbEventLoop::new, 0))?;
203
+ event_loop_class.define_method("create_proxy", method!(RbEventLoop::create_proxy, 0))?;
204
+ event_loop_class.define_method("run", method!(RbEventLoop::run, 1))?;
205
+ Ok(())
206
+ }