tina4 0.3.0 → 0.5.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tina4/version.rb +1 -1
  3. data/lib/tina4ruby.rb +4 -0
  4. metadata +14 -258
  5. data/CHANGELOG.md +0 -73
  6. data/LICENSE.txt +0 -21
  7. data/README.md +0 -662
  8. data/exe/tina4 +0 -4
  9. data/lib/tina4/api.rb +0 -152
  10. data/lib/tina4/auth.rb +0 -139
  11. data/lib/tina4/cli.rb +0 -243
  12. data/lib/tina4/crud.rb +0 -124
  13. data/lib/tina4/database.rb +0 -135
  14. data/lib/tina4/database_result.rb +0 -89
  15. data/lib/tina4/debug.rb +0 -83
  16. data/lib/tina4/dev_reload.rb +0 -68
  17. data/lib/tina4/drivers/firebird_driver.rb +0 -94
  18. data/lib/tina4/drivers/mssql_driver.rb +0 -112
  19. data/lib/tina4/drivers/mysql_driver.rb +0 -90
  20. data/lib/tina4/drivers/postgres_driver.rb +0 -99
  21. data/lib/tina4/drivers/sqlite_driver.rb +0 -85
  22. data/lib/tina4/env.rb +0 -55
  23. data/lib/tina4/field_types.rb +0 -84
  24. data/lib/tina4/graphql.rb +0 -837
  25. data/lib/tina4/localization.rb +0 -100
  26. data/lib/tina4/middleware.rb +0 -59
  27. data/lib/tina4/migration.rb +0 -124
  28. data/lib/tina4/orm.rb +0 -168
  29. data/lib/tina4/queue.rb +0 -117
  30. data/lib/tina4/queue_backends/kafka_backend.rb +0 -80
  31. data/lib/tina4/queue_backends/lite_backend.rb +0 -79
  32. data/lib/tina4/queue_backends/rabbitmq_backend.rb +0 -73
  33. data/lib/tina4/rack_app.rb +0 -150
  34. data/lib/tina4/request.rb +0 -158
  35. data/lib/tina4/response.rb +0 -172
  36. data/lib/tina4/router.rb +0 -142
  37. data/lib/tina4/scss_compiler.rb +0 -131
  38. data/lib/tina4/session.rb +0 -145
  39. data/lib/tina4/session_handlers/file_handler.rb +0 -55
  40. data/lib/tina4/session_handlers/mongo_handler.rb +0 -49
  41. data/lib/tina4/session_handlers/redis_handler.rb +0 -43
  42. data/lib/tina4/swagger.rb +0 -123
  43. data/lib/tina4/template.rb +0 -478
  44. data/lib/tina4/templates/base.twig +0 -25
  45. data/lib/tina4/templates/errors/403.twig +0 -22
  46. data/lib/tina4/templates/errors/404.twig +0 -22
  47. data/lib/tina4/templates/errors/500.twig +0 -22
  48. data/lib/tina4/testing.rb +0 -213
  49. data/lib/tina4/webserver.rb +0 -101
  50. data/lib/tina4/websocket.rb +0 -167
  51. data/lib/tina4/wsdl.rb +0 -164
  52. data/lib/tina4.rb +0 -234
data/lib/tina4/router.rb DELETED
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tina4
4
- class Route
5
- attr_reader :method, :path, :handler, :auth_handler, :swagger_meta, :path_regex, :param_names
6
-
7
- def initialize(method, path, handler, auth_handler: nil, swagger_meta: {})
8
- @method = method.to_s.upcase.freeze
9
- @path = normalize_path(path).freeze
10
- @handler = handler
11
- @auth_handler = auth_handler
12
- @swagger_meta = swagger_meta
13
- @param_names = []
14
- @path_regex = compile_pattern(@path)
15
- @param_names.freeze
16
- end
17
-
18
- # Returns params hash if matched, false otherwise
19
- def match_path(request_path)
20
- match = @path_regex.match(request_path)
21
- return false unless match
22
-
23
- if @param_names.empty?
24
- # Static route — no params to extract
25
- {}
26
- else
27
- params = {}
28
- @param_names.each_with_index do |param_def, i|
29
- raw_value = match[i + 1]
30
- params[param_def[:name]] = cast_param(raw_value, param_def[:type])
31
- end
32
- params
33
- end
34
- end
35
-
36
- private
37
-
38
- def normalize_path(path)
39
- p = path.to_s.gsub("\\", "/")
40
- p = "/#{p}" unless p.start_with?("/")
41
- p = p.chomp("/") unless p == "/"
42
- p
43
- end
44
-
45
- def compile_pattern(path)
46
- return Regexp.new("\\A/\\z") if path == "/"
47
-
48
- parts = path.split("/").reject(&:empty?)
49
- regex_parts = parts.map do |part|
50
- if part =~ /\A\{(\w+)(?::(\w+))?\}\z/
51
- name = Regexp.last_match(1)
52
- type = Regexp.last_match(2) || "string"
53
- @param_names << { name: name.to_sym, type: type }
54
- case type
55
- when "int", "integer"
56
- '(\d+)'
57
- when "float", "number"
58
- '([\d.]+)'
59
- when "path"
60
- '(.+)'
61
- else
62
- '([^/]+)'
63
- end
64
- else
65
- Regexp.escape(part)
66
- end
67
- end
68
- Regexp.new("\\A/#{regex_parts.join("/")}\\z")
69
- end
70
-
71
- def cast_param(value, type)
72
- case type
73
- when "int", "integer"
74
- value.to_i
75
- when "float", "number"
76
- value.to_f
77
- else
78
- value
79
- end
80
- end
81
- end
82
-
83
- module Router
84
- class << self
85
- def routes
86
- @routes ||= []
87
- end
88
-
89
- # Routes indexed by HTTP method for O(1) method lookup
90
- def method_index
91
- @method_index ||= Hash.new { |h, k| h[k] = [] }
92
- end
93
-
94
- def add_route(method, path, handler, auth_handler: nil, swagger_meta: {})
95
- route = Route.new(method, path, handler, auth_handler: auth_handler, swagger_meta: swagger_meta)
96
- routes << route
97
- method_index[route.method] << route
98
- Tina4::Debug.debug("Route registered: #{method.upcase} #{path}")
99
- route
100
- end
101
-
102
- def find_route(path, method)
103
- normalized_method = method.upcase
104
- # Normalize path once (not per-route)
105
- normalized_path = path.gsub("\\", "/")
106
- normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
107
- normalized_path = normalized_path.chomp("/") unless normalized_path == "/"
108
-
109
- # Only scan routes matching this HTTP method
110
- candidates = method_index[normalized_method]
111
- candidates.each do |route|
112
- params = route.match_path(normalized_path)
113
- return [route, params] if params
114
- end
115
- nil
116
- end
117
-
118
- def clear!
119
- @routes = []
120
- @method_index = Hash.new { |h, k| h[k] = [] }
121
- end
122
-
123
- def group(prefix, auth_handler: nil, &block)
124
- GroupContext.new(prefix, auth_handler).instance_eval(&block)
125
- end
126
- end
127
-
128
- class GroupContext
129
- def initialize(prefix, auth_handler = nil)
130
- @prefix = prefix.chomp("/")
131
- @auth_handler = auth_handler
132
- end
133
-
134
- %w[get post put patch delete any].each do |m|
135
- define_method(m) do |path, swagger_meta: {}, &handler|
136
- full_path = "#{@prefix}#{path}"
137
- Tina4::Router.add_route(m, full_path, handler, auth_handler: @auth_handler, swagger_meta: swagger_meta)
138
- end
139
- end
140
- end
141
- end
142
- end
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
- require "fileutils"
3
-
4
- module Tina4
5
- module ScssCompiler
6
- SCSS_DIRS = %w[src/scss scss src/styles styles].freeze
7
- CSS_OUTPUT = "public/css"
8
-
9
- class << self
10
- def compile_all(root_dir = Dir.pwd)
11
- output_dir = File.join(root_dir, CSS_OUTPUT)
12
- FileUtils.mkdir_p(output_dir)
13
-
14
- SCSS_DIRS.each do |dir|
15
- scss_dir = File.join(root_dir, dir)
16
- next unless Dir.exist?(scss_dir)
17
-
18
- Dir.glob(File.join(scss_dir, "**/*.scss")).each do |scss_file|
19
- next if File.basename(scss_file).start_with?("_") # Skip partials
20
- compile_file(scss_file, output_dir, scss_dir)
21
- end
22
- end
23
- end
24
-
25
- def compile_file(scss_file, output_dir, base_dir)
26
- relative = scss_file.sub(base_dir, "").sub(/\.scss$/, ".css")
27
- css_file = File.join(output_dir, relative)
28
- FileUtils.mkdir_p(File.dirname(css_file))
29
-
30
- scss_content = File.read(scss_file)
31
- css_content = compile_scss(scss_content, File.dirname(scss_file))
32
- File.write(css_file, css_content)
33
-
34
- Tina4::Debug.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
35
- rescue => e
36
- Tina4::Debug.error("SCSS compilation failed: #{scss_file} - #{e.message}")
37
- end
38
-
39
- def compile_scss(content, base_dir)
40
- # Try sassc gem first
41
- begin
42
- require "sassc"
43
- return SassC::Engine.new(content, style: :expanded, load_paths: [base_dir]).render
44
- rescue LoadError
45
- # Fall through to basic compiler
46
- end
47
-
48
- # Basic SCSS to CSS conversion (handles common patterns)
49
- basic_compile(content, base_dir)
50
- end
51
-
52
- private
53
-
54
- def basic_compile(content, base_dir)
55
- # Handle @import
56
- content = process_imports(content, base_dir)
57
-
58
- # Handle variables
59
- variables = {}
60
- content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/) do
61
- variables[Regexp.last_match(1)] = Regexp.last_match(2).strip
62
- ""
63
- end
64
-
65
- # Replace variable references
66
- variables.each do |name, value|
67
- content = content.gsub("$#{name}", value)
68
- end
69
-
70
- # Handle nesting (basic single-level)
71
- content = flatten_nesting(content)
72
-
73
- # Handle & parent selector
74
- content = content.gsub(/&/, "")
75
-
76
- content
77
- end
78
-
79
- def process_imports(content, base_dir)
80
- content.gsub(/@import\s+["'](.+?)["']\s*;/) do
81
- import_path = Regexp.last_match(1)
82
- candidates = [
83
- File.join(base_dir, "#{import_path}.scss"),
84
- File.join(base_dir, "_#{import_path}.scss"),
85
- File.join(base_dir, import_path)
86
- ]
87
- found = candidates.find { |c| File.exist?(c) }
88
- if found
89
- imported = File.read(found)
90
- process_imports(imported, File.dirname(found))
91
- else
92
- "/* import not found: #{import_path} */"
93
- end
94
- end
95
- end
96
-
97
- def flatten_nesting(content)
98
- # Very basic nesting flattener - handles single level
99
- result = ""
100
- content.scan(/([^{]+)\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/m) do |selector, body|
101
- selector = selector.strip
102
- # Check for nested rules
103
- if body =~ /\{/
104
- # Has nested content
105
- props = ""
106
- nested = ""
107
- body.scan(/([^{;]+(?:\{[^}]*\}|;))/m) do |part_arr|
108
- part = part_arr[0].strip
109
- if part.include?("{")
110
- # Nested rule
111
- if part =~ /\A(.+?)\s*\{(.*?)\}\s*\z/m
112
- nested_sel = Regexp.last_match(1).strip
113
- nested_body = Regexp.last_match(2).strip
114
- full_sel = "#{selector} #{nested_sel}"
115
- nested += "#{full_sel} { #{nested_body} }\n"
116
- end
117
- else
118
- props += " #{part}\n" unless part.empty?
119
- end
120
- end
121
- result += "#{selector} {\n#{props}}\n" unless props.strip.empty?
122
- result += nested
123
- else
124
- result += "#{selector} {\n#{body}\n}\n"
125
- end
126
- end
127
- result.empty? ? content : result
128
- end
129
- end
130
- end
131
- end
data/lib/tina4/session.rb DELETED
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
- require "securerandom"
3
- require "json"
4
-
5
- module Tina4
6
- class Session
7
- DEFAULT_OPTIONS = {
8
- cookie_name: "tina4_session",
9
- secret: nil,
10
- max_age: 86400,
11
- handler: :file,
12
- handler_options: {}
13
- }.freeze
14
-
15
- attr_reader :id, :data
16
-
17
- def initialize(env, options = {})
18
- @options = DEFAULT_OPTIONS.merge(options)
19
- @options[:secret] ||= ENV["SECRET"] || "tina4-default-secret"
20
- @handler = create_handler
21
- @id = extract_session_id(env) || SecureRandom.hex(32)
22
- @data = load_session
23
- @modified = false
24
- end
25
-
26
- def [](key)
27
- @data[key.to_s]
28
- end
29
-
30
- def []=(key, value)
31
- @data[key.to_s] = value
32
- @modified = true
33
- end
34
-
35
- def delete(key)
36
- @data.delete(key.to_s)
37
- @modified = true
38
- end
39
-
40
- def clear
41
- @data = {}
42
- @modified = true
43
- end
44
-
45
- def to_hash
46
- @data.dup
47
- end
48
-
49
- def save
50
- return unless @modified
51
- @handler.write(@id, @data)
52
- @modified = false
53
- end
54
-
55
- def destroy
56
- @handler.destroy(@id)
57
- @data = {}
58
- end
59
-
60
- def cookie_header
61
- "#{@options[:cookie_name]}=#{@id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{@options[:max_age]}"
62
- end
63
-
64
- private
65
-
66
- def extract_session_id(env)
67
- cookie_str = env["HTTP_COOKIE"] || ""
68
- cookie_str.split(";").each do |pair|
69
- key, value = pair.strip.split("=", 2)
70
- return value if key == @options[:cookie_name]
71
- end
72
- nil
73
- end
74
-
75
- def load_session
76
- existing = @handler.read(@id)
77
- existing || {}
78
- end
79
-
80
- def create_handler
81
- case @options[:handler].to_sym
82
- when :file
83
- Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
84
- when :redis
85
- Tina4::SessionHandlers::RedisHandler.new(@options[:handler_options])
86
- when :mongo, :mongodb
87
- Tina4::SessionHandlers::MongoHandler.new(@options[:handler_options])
88
- else
89
- Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
90
- end
91
- end
92
- end
93
-
94
- class LazySession
95
- def initialize(env, options = {})
96
- @env = env
97
- @options = options
98
- @session = nil
99
- end
100
-
101
- def [](key)
102
- ensure_loaded
103
- @session[key]
104
- end
105
-
106
- def []=(key, value)
107
- ensure_loaded
108
- @session[key] = value
109
- end
110
-
111
- def delete(key)
112
- ensure_loaded
113
- @session.delete(key)
114
- end
115
-
116
- def clear
117
- ensure_loaded
118
- @session.clear
119
- end
120
-
121
- def save
122
- @session&.save
123
- end
124
-
125
- def destroy
126
- @session&.destroy
127
- end
128
-
129
- def cookie_header
130
- ensure_loaded
131
- @session.cookie_header
132
- end
133
-
134
- def to_hash
135
- ensure_loaded
136
- @session.to_hash
137
- end
138
-
139
- private
140
-
141
- def ensure_loaded
142
- @session ||= Session.new(@env, @options)
143
- end
144
- end
145
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
- require "fileutils"
4
-
5
- module Tina4
6
- module SessionHandlers
7
- class FileHandler
8
- def initialize(options = {})
9
- @dir = options[:dir] || File.join(Dir.pwd, "sessions")
10
- @ttl = options[:ttl] || 86400
11
- FileUtils.mkdir_p(@dir)
12
- end
13
-
14
- def read(session_id)
15
- path = session_path(session_id)
16
- return nil unless File.exist?(path)
17
-
18
- # Check expiry
19
- if File.mtime(path) + @ttl < Time.now
20
- File.delete(path)
21
- return nil
22
- end
23
-
24
- data = File.read(path)
25
- JSON.parse(data)
26
- rescue JSON::ParserError
27
- nil
28
- end
29
-
30
- def write(session_id, data)
31
- path = session_path(session_id)
32
- File.write(path, JSON.generate(data))
33
- end
34
-
35
- def destroy(session_id)
36
- path = session_path(session_id)
37
- File.delete(path) if File.exist?(path)
38
- end
39
-
40
- def cleanup
41
- return unless Dir.exist?(@dir)
42
- Dir.glob(File.join(@dir, "sess_*")).each do |file|
43
- File.delete(file) if File.mtime(file) + @ttl < Time.now
44
- end
45
- end
46
-
47
- private
48
-
49
- def session_path(session_id)
50
- safe_id = session_id.gsub(/[^a-zA-Z0-9_-]/, "")
51
- File.join(@dir, "sess_#{safe_id}.json")
52
- end
53
- end
54
- end
55
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
-
4
- module Tina4
5
- module SessionHandlers
6
- class MongoHandler
7
- def initialize(options = {})
8
- require "mongo"
9
- @ttl = options[:ttl] || 86400
10
- client = Mongo::Client.new(
11
- options[:uri] || "mongodb://localhost:27017",
12
- database: options[:database] || "tina4_sessions"
13
- )
14
- @collection = client[options[:collection] || "sessions"]
15
- # Ensure TTL index
16
- @collection.indexes.create_one(
17
- { updated_at: 1 },
18
- expire_after_seconds: @ttl
19
- )
20
- rescue LoadError
21
- raise "MongoDB session handler requires the 'mongo' gem. Install with: gem install mongo"
22
- rescue Mongo::Error => e
23
- Tina4::Debug.error("MongoDB session setup failed: #{e.message}")
24
- end
25
-
26
- def read(session_id)
27
- doc = @collection.find(_id: session_id).first
28
- return nil unless doc
29
- doc["data"]
30
- end
31
-
32
- def write(session_id, data)
33
- @collection.update_one(
34
- { _id: session_id },
35
- { "$set" => { data: data, updated_at: Time.now } },
36
- upsert: true
37
- )
38
- end
39
-
40
- def destroy(session_id)
41
- @collection.delete_one(_id: session_id)
42
- end
43
-
44
- def cleanup
45
- # MongoDB TTL index handles cleanup
46
- end
47
- end
48
- end
49
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
-
4
- module Tina4
5
- module SessionHandlers
6
- class RedisHandler
7
- def initialize(options = {})
8
- require "redis"
9
- @prefix = options[:prefix] || "tina4:session:"
10
- @ttl = options[:ttl] || 86400
11
- @redis = Redis.new(
12
- host: options[:host] || "localhost",
13
- port: options[:port] || 6379,
14
- db: options[:db] || 0,
15
- password: options[:password]
16
- )
17
- rescue LoadError
18
- raise "Redis session handler requires the 'redis' gem. Install with: gem install redis"
19
- end
20
-
21
- def read(session_id)
22
- data = @redis.get("#{@prefix}#{session_id}")
23
- return nil unless data
24
- JSON.parse(data)
25
- rescue JSON::ParserError
26
- nil
27
- end
28
-
29
- def write(session_id, data)
30
- key = "#{@prefix}#{session_id}"
31
- @redis.setex(key, @ttl, JSON.generate(data))
32
- end
33
-
34
- def destroy(session_id)
35
- @redis.del("#{@prefix}#{session_id}")
36
- end
37
-
38
- def cleanup
39
- # Redis handles TTL automatically
40
- end
41
- end
42
- end
43
- end
data/lib/tina4/swagger.rb DELETED
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
-
4
- module Tina4
5
- module Swagger
6
- class << self
7
- def generate
8
- spec = base_spec
9
- Tina4::Router.routes.each do |route|
10
- add_route_to_spec(spec, route)
11
- end
12
- spec
13
- end
14
-
15
- private
16
-
17
- def base_spec
18
- {
19
- "openapi" => "3.0.3",
20
- "info" => {
21
- "title" => ENV["PROJECT_NAME"] || "Tina4 Ruby API",
22
- "version" => ENV["VERSION"] || Tina4::VERSION,
23
- "description" => "Auto-generated API documentation"
24
- },
25
- "servers" => [
26
- { "url" => "/" }
27
- ],
28
- "paths" => {},
29
- "components" => {
30
- "securitySchemes" => {
31
- "bearerAuth" => {
32
- "type" => "http",
33
- "scheme" => "bearer",
34
- "bearerFormat" => "JWT"
35
- }
36
- }
37
- }
38
- }
39
- end
40
-
41
- def add_route_to_spec(spec, route)
42
- path = convert_path(route.path)
43
- method = route.method.downcase
44
- return if method == "any"
45
-
46
- spec["paths"][path] ||= {}
47
- operation = {
48
- "summary" => route.swagger_meta[:summary] || "#{method.upcase} #{route.path}",
49
- "description" => route.swagger_meta[:description] || "",
50
- "tags" => route.swagger_meta[:tags] || [extract_tag(route.path)],
51
- "parameters" => build_parameters(route),
52
- "responses" => route.swagger_meta[:responses] || default_responses
53
- }
54
-
55
- if route.auth_handler
56
- operation["security"] = [{ "bearerAuth" => [] }]
57
- end
58
-
59
- if %w[post put patch].include?(method) && route.swagger_meta[:request_body]
60
- operation["requestBody"] = route.swagger_meta[:request_body]
61
- elsif %w[post put patch].include?(method)
62
- operation["requestBody"] = default_request_body
63
- end
64
-
65
- spec["paths"][path][method] = operation
66
- end
67
-
68
- def convert_path(path)
69
- # Convert {id:int} to {id}
70
- path.gsub(/\{(\w+)(?::\w+)?\}/, '{\1}')
71
- end
72
-
73
- def extract_tag(path)
74
- parts = path.split("/").reject(&:empty?)
75
- parts.first || "default"
76
- end
77
-
78
- def build_parameters(route)
79
- params = []
80
- route.param_names.each do |param|
81
- params << {
82
- "name" => param[:name].to_s,
83
- "in" => "path",
84
- "required" => true,
85
- "schema" => param_schema(param[:type])
86
- }
87
- end
88
- params
89
- end
90
-
91
- def param_schema(type)
92
- case type
93
- when "int", "integer"
94
- { "type" => "integer" }
95
- when "float", "number"
96
- { "type" => "number" }
97
- else
98
- { "type" => "string" }
99
- end
100
- end
101
-
102
- def default_responses
103
- {
104
- "200" => { "description" => "Successful response" },
105
- "400" => { "description" => "Bad request" },
106
- "401" => { "description" => "Unauthorized" },
107
- "404" => { "description" => "Not found" },
108
- "500" => { "description" => "Internal server error" }
109
- }
110
- end
111
-
112
- def default_request_body
113
- {
114
- "content" => {
115
- "application/json" => {
116
- "schema" => { "type" => "object" }
117
- }
118
- }
119
- }
120
- end
121
- end
122
- end
123
- end