tina4ruby 3.13.81 → 3.13.82

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c97745aeda99520ca771efcba237145e03897174113229656c05f12f1b3cc07
4
- data.tar.gz: 64e21ce431b7c697cc9b392f810d0b5072d6245d38e2c60d962a57a9c473e174
3
+ metadata.gz: 3c8f9a8a50b4bb94a8b19b81774ce30e7a9d64fca932c2eb6a49016dc0352171
4
+ data.tar.gz: 90066d82b0085d019c3118fe57d225eb78206fe44f55c235dd3e7d67d8451382
5
5
  SHA512:
6
- metadata.gz: 6327789d45d5e1d44a6f6641be57580c69587fa3f935b07d8c5511b0754b4cc307cd0746299a0e4c57f67dcbf982afc58e331c160a06b75445568b75034a0b76
7
- data.tar.gz: f4235dd14f3fe8f6aee9ef35705761bb0191694b3942f6d7858e6eabdf883c8421b2b5674515d87bd4054d65c32423ab759123e7c699b629d2abd43d99d822d9
6
+ metadata.gz: 042b4dbfa4faf371dde9eb25e3e4406170c46086c29f1ff67afecad0fb61c482605f4f912469251b8640545142eeb295dd7eb1797081137575d941ebccf3f9be
7
+ data.tar.gz: e5e303934c51d2fc83692817eef9f26382d5bbd5c1b735f6a0b3bcd65a13a7a902574b070cd86a1b60cf0e17a34788444507c5b8cd62c9d8a60aea626abff80f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ Tina4 keeps ONE version across all four frameworks (Python, PHP, Ruby, Node.js), so a version
4
+ number means the same thing everywhere.
5
+
6
+ **The authoritative release notes for every shipped version live in the documentation:**
7
+ https://tina4.com/ruby/36-releases
8
+
9
+ This file is deliberately NOT a copy of those notes. Duplicating them is exactly how a
10
+ changelog rots into claiming a version that was never cut, so this file records only
11
+ UNRELEASED work. When a version ships, its notes go to the release notes above.
12
+
13
+ ## Unreleased
14
+
15
+ ### Added
16
+
17
+ - **MQTT 3.1.1 client** (`Tina4::Mqtt` / `Tina4::MqttMessage`), zero-dependency (stdlib `socket` +
18
+ lazily-required `openssl`), verified against a real broker with no mocks. publish/subscribe/consume,
19
+ QoS 0/1, retained, Last Will, per-client TLS, QoS 2 refused loudly. Ruby ships all 97 shared
20
+ features plus its native ERB engine, for **98 built-in features**.
21
+
22
+ ### Changed
23
+
24
+ - Internal: the SQL dialect-translation file is renamed
25
+ `lib/tina4/sql_translation.rb` -> `lib/tina4/sql_translator.rb`, so the filename matches the
26
+ `Tina4::SQLTranslator` class it defines (and the sibling frameworks). The class name, its
27
+ methods and its behaviour are unchanged, and `require "tina4"` is unaffected - this is a
28
+ filename alignment, not an API change. Only code that bypassed the gem's own entry point with
29
+ a direct `require "tina4/sql_translation"` needs to update the path.
30
+
31
+ ## Earlier history (pre-3.x)
32
+
33
+ Kept for reference only. The versions below are from the 0.x line, long before the unified
34
+ 3.x versioning; everything from 3.x onward is in the release notes linked above.
35
+
3
36
  ## [0.4.0] - 2026-03-18
4
37
 
5
38
  ### Added
@@ -14,7 +47,7 @@
14
47
  - Recursive descent GraphQL parser (queries, mutations, fragments, variables, aliases)
15
48
  - Depth-first AST executor with resolver pattern
16
49
  - GraphQL schema with programmatic type registration
17
- - ORM auto-schema generation (`schema.from_orm(User)`) auto-creates CRUD queries/mutations
50
+ - ORM auto-schema generation (`schema.from_orm(User)`) - auto-creates CRUD queries/mutations
18
51
  - GraphiQL UI served at GET /graphql
19
52
  - Route integration via `gql.register_route("/graphql")`
20
53
  - Full GraphQL type system (scalars, objects, lists, non-null, input objects)
@@ -23,7 +56,7 @@
23
56
 
24
57
  ### Added
25
58
  - Default auth protection for POST/PUT/PATCH/DELETE routes (matching tina4_python behavior)
26
- - API_KEY bypass in bearer auth if `ENV["API_KEY"]` matches the bearer token, access is granted
59
+ - API_KEY bypass in bearer auth - if `ENV["API_KEY"]` matches the bearer token, access is granted
27
60
  - `auth: false` option to make write routes public (equivalent to tina4_python's `@noauth()`)
28
61
  - `default_secure_auth` cached auth handler for performance
29
62
  - `resolve_auth` helper for flexible auth resolution
data/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
  <h1 align="center">Tina4 Ruby</h1>
5
5
  <h3 align="center">TINA4: The Intelligent Native Application 4ramework</h3>
6
6
  <p align="center"><em>Simple. Fast. Human. &nbsp;|&nbsp; Built for AI. Built for you.</em></p>
7
- <p align="center">55 built-in features. Zero runtime dependencies. One require, everything works.</p>
7
+ <p align="center">98 built-in features. Zero runtime dependencies. One require, everything works.</p>
8
8
  <p align="center">
9
9
  <a href="https://rubygems.org/gems/tina4ruby"><img src="https://img.shields.io/gem/v/tina4ruby?color=7b1fa2&label=RubyGems" alt="RubyGems"></a>
10
10
  <img src="https://img.shields.io/badge/tests-2%2C508%20passing-brightgreen" alt="Tests">
11
- <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
11
+ <img src="https://img.shields.io/badge/features-98-blue" alt="Features">
12
12
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
13
13
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
14
14
  </p>
@@ -91,7 +91,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
91
91
  | **Tina4 Ruby** | **10,243** | 0 | 55 |
92
92
  | Sinatra | 9,548 | 5+ | ~4 |
93
93
 
94
- Tina4 Ruby outperforms Sinatra while delivering **55 features vs ~4**, with zero runtime dependencies.
94
+ Tina4 Ruby outperforms Sinatra while delivering **98 features vs ~4**, with zero runtime dependencies.
95
95
 
96
96
  **Across all 4 Tina4 implementations:**
97
97
 
@@ -105,7 +105,7 @@ Tina4 Ruby outperforms Sinatra while delivering **55 features vs ~4**, with zero
105
105
 
106
106
  ## Cross-Framework Parity
107
107
 
108
- Tina4 ships identical features across four languages: same architecture, same conventions, same 55 features:
108
+ Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
109
109
 
110
110
  | | Python | PHP | Ruby | Node.js |
111
111
  |---|--------|-----|------|---------|
@@ -36,15 +36,34 @@ module Tina4
36
36
  end
37
37
 
38
38
  # Stop and join every running task. Called on graceful shutdown.
39
+ #
40
+ # Each stop_task deregisters its own descriptor, so there is no blanket
41
+ # `tasks.clear` here: clearing would ALSO drop a task registered while
42
+ # this loop was running — leaving its thread alive but invisible in the
43
+ # registry, which is the worse of the two failure modes.
39
44
  def stop_all(timeout: 2.0)
40
45
  snapshot = mutex.synchronize { tasks.dup }
41
46
  snapshot.each { |task| stop_task(task, timeout: timeout) }
42
- mutex.synchronize { tasks.clear }
43
47
  end
44
48
 
45
- # Stop a single task. Used by tests that register, fire, then stop.
49
+ # Stop a single task and DEREGISTER it. Used by tests that register, fire,
50
+ # then stop, and by any subsystem that owns a task for part of its life
51
+ # (e.g. Mqtt::Client#stop_keepalive).
52
+ #
53
+ # The descriptor is removed from `tasks` so the registry never reports a
54
+ # stopped task as registered — leaving it in place made `tasks` grow for
55
+ # the life of the process on every start/stop cycle and made introspection
56
+ # lie about what is actually running.
57
+ #
58
+ # Idempotent: a second call on the same descriptor removes nothing, finds
59
+ # no thread and returns safely.
46
60
  def stop_task(task, timeout: 2.0)
47
61
  task[:running] = false
62
+ # Identity, not equality: `tasks.delete(task)` uses `==`, which would
63
+ # take out any OTHER descriptor that happens to hold an equal Hash
64
+ # (same callback, same interval). Only this exact descriptor goes.
65
+ mutex.synchronize { tasks.delete_if { |registered| registered.equal?(task) } }
66
+
48
67
  thread = task[:thread]
49
68
  return unless thread
50
69
 
data/lib/tina4/cli.rb CHANGED
@@ -75,6 +75,58 @@ module Tina4
75
75
  "help" => { handler: :cmd_help, summary: "Show this help message" },
76
76
  }.freeze
77
77
 
78
+ # ── Delegation to the `tina4` client ────────────────────────────────
79
+ #
80
+ # `doctor`, `setup` and `deploy` are owned by the Rust `tina4` client, not by
81
+ # any framework. `doctor` probes ALL FOUR runtimes plus package managers,
82
+ # ports and global AI-skills currency; `setup` installs language runtimes
83
+ # (Homebrew / Chocolatey, with UAC elevation on Windows) and scaffolds a
84
+ # project from nothing; `deploy` writes deployment boilerplate baked into the
85
+ # client binary. Cloning any of them into four languages would duplicate
86
+ # hundreds of lines per language for zero new capability — and four copies
87
+ # would immediately drift.
88
+ #
89
+ # So the framework CLI DELEGATES: it resolves `tina4` on PATH, runs it with
90
+ # the same argv, and exits with the client's exit code. All four frameworks
91
+ # reach the SAME implementation, which is a stronger parity guarantee than
92
+ # four ports.
93
+ #
94
+ # Delegation is ALLOW-LISTED, never blind. The client forwards ITS unknown
95
+ # commands to the framework CLI, so a framework that forwarded its unknowns
96
+ # back would ping-pong an unknown command between two processes forever. This
97
+ # closed set contains only commands the client dispatches natively, so no loop
98
+ # is possible by construction, and a real typo still gets "Unknown command".
99
+ #
100
+ # There are no handlers here: #run runs `tina4 <name> <args...>` and exits
101
+ # with its code. Keep this set closed and identical in all four frameworks.
102
+ # Summaries are the client's own wording, verbatim. Ruby mirror of the Python
103
+ # master's DELEGATED / _delegate_to_client.
104
+
105
+ DELEGATED = {
106
+ "doctor" => { summary: "Check installed languages and tools" },
107
+ "setup" => { summary: "Guided, menu-driven setup: install everything + scaffold a ready-to-run project" },
108
+ "deploy" => { usage: "<docker|systemd|nginx|cpanel> [--force]", args: ["target"],
109
+ summary: "Generate deployment scaffolding (Dockerfile, systemd unit, nginx block, cPanel)" },
110
+ }.freeze
111
+
112
+ CLIENT_BINARY = "tina4"
113
+
114
+ # Internal process marker (same class as the client's own
115
+ # TINA4_SETUP_ELEVATED): set on the child so a client that resolves back to a
116
+ # framework CLI is caught instead of spawning forever. NOT user configuration
117
+ # — deliberately absent from the CLI's known_vars().
118
+ DELEGATION_GUARD_ENV = "TINA4_CLI_DELEGATED"
119
+
120
+ # 127 is the conventional "command not found" and covers both ways the client
121
+ # can be unreachable (absent from PATH, or the loop guard tripping).
122
+ EXIT_CLIENT_UNAVAILABLE = 127
123
+ EXIT_UNKNOWN_COMMAND = 1
124
+
125
+ CLIENT_INSTALL_HINT = <<~HINT.freeze
126
+ Install it: curl -fsSL https://tina4.com/install.sh | sh
127
+ Windows: irm https://tina4.com/install.ps1 | iex
128
+ HINT
129
+
78
130
  # ── Field type mapping ──────────────────────────────────────────────
79
131
  FIELD_TYPE_MAP = {
80
132
  "string" => { orm: "string_field", sql: "VARCHAR(255)", default: "''" },
@@ -105,15 +157,68 @@ module Tina4
105
157
  spec = COMMANDS[command]
106
158
  if spec
107
159
  send(spec[:handler], argv)
160
+ elsif DELEGATED.key?(command)
161
+ exit delegate_to_client(command, argv)
108
162
  else
163
+ # A genuinely unknown command is an ERROR: exit non-zero so a typo in a
164
+ # script or CI step fails loudly instead of reporting success.
109
165
  puts "Unknown command: #{command}"
110
166
  cmd_help
111
- exit 1
167
+ exit EXIT_UNKNOWN_COMMAND
112
168
  end
113
169
  end
114
170
 
115
171
  private
116
172
 
173
+ # Absolute path of the `tina4` client on PATH, or nil if it isn't there.
174
+ #
175
+ # Scans PATH directly rather than shelling out to which/where — one less
176
+ # process and it behaves the same on every platform.
177
+ def find_client
178
+ windows = RUBY_PLATFORM =~ /mswin|mingw|cygwin/
179
+ names = windows ? %W[#{CLIENT_BINARY}.exe #{CLIENT_BINARY}.cmd #{CLIENT_BINARY}.bat] : [CLIENT_BINARY]
180
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
181
+ next if dir.empty?
182
+
183
+ names.each do |name|
184
+ candidate = File.join(dir, name)
185
+ return candidate if File.file?(candidate) && (windows || File.executable?(candidate))
186
+ end
187
+ end
188
+ nil
189
+ end
190
+
191
+ # Run `tina4 <command> <args...>`, returning the client's exit code.
192
+ #
193
+ # Returns EXIT_CLIENT_UNAVAILABLE (127) with an actionable message when the
194
+ # client is not on PATH, or when the re-entry guard shows the resolved `tina4`
195
+ # came back to a framework CLI (a delegation loop).
196
+ def delegate_to_client(command, args)
197
+ if ENV[DELEGATION_GUARD_ENV] == command
198
+ warn " Refusing to delegate '#{command}' again — the 'tina4' on your PATH"
199
+ warn " resolved back to a framework CLI instead of the tina4 client."
200
+ warn ""
201
+ warn " Check which 'tina4' comes first on your PATH and put the client first."
202
+ return EXIT_CLIENT_UNAVAILABLE
203
+ end
204
+
205
+ client = find_client
206
+ if client.nil?
207
+ warn " '#{command}' is provided by the tina4 client, which is not on your PATH."
208
+ warn ""
209
+ CLIENT_INSTALL_HINT.each_line { |line| warn " #{line.chomp}" }
210
+ warn ""
211
+ warn " Then run: #{CLIENT_BINARY} #{command}"
212
+ return EXIT_CLIENT_UNAVAILABLE
213
+ end
214
+
215
+ # Array form => no shell, so no quoting/injection surface. stdio is
216
+ # inherited, so the client's interactive prompts (setup) and colour output
217
+ # work exactly as if it had been invoked directly.
218
+ system({ DELEGATION_GUARD_ENV => command }, client, command, *args)
219
+ $?&.exitstatus || EXIT_CLIENT_UNAVAILABLE
220
+ end
221
+
117
222
  # ── Helpers ──────────────────────────────────────────────────────────
118
223
 
119
224
  # CamelCase -> snake_case: ProductCategory -> product_category
@@ -2083,13 +2188,12 @@ module Tina4
2083
2188
  content = <<~RUBY
2084
2189
  # #{name} request validator.
2085
2190
  #
2086
- # Tina4::Validator is NOT part of the default `require "tina4"` surface
2087
- # (unlike Queue / Events / ServiceRunner), so require it explicitly here.
2088
- # NOT auto-loaded require this file from the route that validates:
2191
+ # Tina4::Validator comes with `require "tina4"` — no extra require needed.
2192
+ # This FILE is not auto-discovered though (only src/routes/ and src/orm/
2193
+ # are), so require it from the route that validates:
2089
2194
  # require_relative "../validators/#{snake}"
2090
2195
  # v = validate_#{snake}(request.body)
2091
2196
  # next response.json({ error: v.errors.first[:message] }, 400) unless v.is_valid?
2092
- require "tina4/validator"
2093
2197
 
2094
2198
  def validate_#{snake}(data)
2095
2199
  validator = Tina4::Validator.new(data)
@@ -2756,20 +2860,23 @@ module Tina4
2756
2860
 
2757
2861
  # Print the human-readable command reference.
2758
2862
  #
2759
- # Generated from the COMMANDS and GENERATORS registries — the SAME single
2760
- # source of truth that drives dispatch (#run / #cmd_generate) and the
2863
+ # Generated from the COMMANDS, DELEGATED and GENERATORS registries — the SAME
2864
+ # single source of truth that drives dispatch (#run / #cmd_generate) and the
2761
2865
  # `commands --json` manifest — so the help text can never drift from what
2762
2866
  # the CLI actually does.
2763
2867
  def cmd_help(_argv = nil)
2764
2868
  command_rows = COMMANDS.map do |name, spec|
2765
2869
  ["#{name} #{spec[:usage]}".rstrip, spec[:summary]]
2766
2870
  end
2871
+ delegated_rows = DELEGATED.map do |name, spec|
2872
+ ["#{name} #{spec[:usage]}".rstrip, spec[:summary]]
2873
+ end
2767
2874
  generator_rows = GENERATORS.map do |name, spec|
2768
2875
  ["generate #{name} #{spec[:usage]}".rstrip, spec[:summary]]
2769
2876
  end
2770
2877
  # Align summaries in a column; a left cell longer than the cap overflows
2771
2878
  # cleanly (2-space gap) rather than pushing every other summary out.
2772
- pad = [46, (command_rows + generator_rows).map { |left, _| left.length }.max].min
2879
+ pad = [46, (command_rows + delegated_rows + generator_rows).map { |left, _| left.length }.max].min
2773
2880
 
2774
2881
  row = lambda do |left, summary|
2775
2882
  gap = left.length <= pad ? pad : left.length
@@ -2778,6 +2885,10 @@ module Tina4
2778
2885
 
2779
2886
  lines = ["Tina4 Ruby CLI", "", "Usage: tina4ruby COMMAND [options]", "", "Commands:"]
2780
2887
  lines += command_rows.map { |left, summary| row.call(left, summary) }
2888
+ lines += ["", "Delegated to the #{CLIENT_BINARY} client (same behaviour in every framework):"]
2889
+ lines += delegated_rows.map { |left, summary| row.call(left, summary) }
2890
+ lines += [" (these run the #{CLIENT_BINARY} client — install: " \
2891
+ "curl -fsSL https://tina4.com/install.sh | sh)"]
2781
2892
  lines += ["", "Generators:"]
2782
2893
  lines += generator_rows.map { |left, summary| row.call(left, summary) }
2783
2894
  lines += [
@@ -2809,14 +2920,19 @@ module Tina4
2809
2920
 
2810
2921
  # Build the machine-readable manifest of the CLI's command surface.
2811
2922
  #
2812
- # Pure data: reads the COMMANDS registry and the framework version — no
2813
- # bootstrap, no database, no migrations, no app imports. This is exactly
2814
- # what `commands --json` serializes and what the tina4 client consumes to
2815
- # discover which commands this framework supports.
2923
+ # Pure data: reads the COMMANDS and DELEGATED registries and the framework
2924
+ # version — no bootstrap, no database, no migrations, no app imports. This is
2925
+ # exactly what `commands --json` serializes and what the tina4 client consumes
2926
+ # to discover which commands this framework supports.
2927
+ #
2928
+ # Commands handed to the `tina4` client carry "delegated" => true, so the
2929
+ # manifest describes the WHOLE surface the CLI accepts while still saying who
2930
+ # implements each one. The client needs no change: its help renderer already
2931
+ # drops manifest names that clash with its own natives.
2816
2932
  #
2817
2933
  # Shape:
2818
2934
  # { "framework" => "ruby", "version" => "<x.y.z>",
2819
- # "commands" => [ { "name", "summary", "args"?, "subcommands"? }, ... ] }
2935
+ # "commands" => [ { "name", "summary", "args"?, "subcommands"?, "delegated"? }, ... ] }
2820
2936
  def commands_manifest
2821
2937
  require_relative "version"
2822
2938
  commands = COMMANDS.map do |name, spec|
@@ -2825,6 +2941,11 @@ module Tina4
2825
2941
  entry["subcommands"] = spec[:subcommands].dup if spec[:subcommands]
2826
2942
  entry
2827
2943
  end
2944
+ commands += DELEGATED.map do |name, spec|
2945
+ entry = { "name" => name, "summary" => spec[:summary], "delegated" => true }
2946
+ entry["args"] = spec[:args].dup if spec[:args]
2947
+ entry
2948
+ end
2828
2949
  { "framework" => "ruby", "version" => Tina4::VERSION, "commands" => commands }
2829
2950
  end
2830
2951
 
@@ -2851,7 +2972,8 @@ module Tina4
2851
2972
  puts "\nTina4 #{manifest['framework']} - #{manifest['version']}\n\n"
2852
2973
  width = manifest["commands"].map { |command| command["name"].length }.max
2853
2974
  manifest["commands"].each do |command|
2854
- puts " #{command['name'].ljust(width)} #{command['summary']}"
2975
+ marker = command["delegated"] ? " (#{CLIENT_BINARY} client)" : ""
2976
+ puts " #{command['name'].ljust(width)} #{command['summary']}#{marker}"
2855
2977
  if command["subcommands"]
2856
2978
  puts " #{''.ljust(width)} #{command['subcommands'].join(', ')}"
2857
2979
  end
@@ -3052,6 +3174,11 @@ module Tina4
3052
3174
  {% block content %}{% endblock %}
3053
3175
  <script src="/js/tina4.min.js"></script>
3054
3176
  <script src="/js/frond.min.js"></script>
3177
+ <!-- tina4-js reactive frontend (signals, components, router).
3178
+ Served from the framework's bundled public dir, same as
3179
+ tina4-python/example/src/templates/base.twig:47-48 and
3180
+ tina4-php/example/src/templates/base.twig:47-48. -->
3181
+ <script src="/js/tina4js.min.js"></script>
3055
3182
  {% block scripts %}{% endblock %}
3056
3183
  </body>
3057
3184
  </html>