gemstack 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 39317af6119a78833f6f2223b54d72f629cf72ed2d1a1d87d6c1dd5b09b1d755
4
+ data.tar.gz: 77c7d2c8616cad5484be73a0b2b5e22638db88b6be21b3f4c1e33b6311f11d1e
5
+ SHA512:
6
+ metadata.gz: 2446525d9b09584be6a6d6bbf21a858b97a48ef177d7c8011c79c968e057bae21de4d181a965077c64a2bbcd3cd802c998bb4af4777e910b9226b19f1dcb491e
7
+ data.tar.gz: aa46e6be80e88973f82760d83f299e92346ce78ea176b6ea7ec65146257f59cb95dc722b92d96301fe9d6b16134453b858f64c254bde438429a6fb68da232c2e
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shoaib Malik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # gemstack
2
+
3
+ GemStack: a fast, modular Ruby API framework for Next.js applications.
4
+
5
+ Part of [GemStack](https://github.com/gemstack-rb/gemstack), a modular Ruby API framework for Next.js
6
+ applications. All GemStack gems are developed together in that repository and released with the same
7
+ version.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ gem install gemstack
13
+ gemstack new myapp
14
+ ```
15
+
16
+ This gem is the framework: it depends on every module an app needs (core, cache, schema, http, contract, dev, cli). Optional modules are added per app.
17
+
18
+ ## Documentation
19
+
20
+ - [Guide](https://github.com/gemstack-rb/gemstack/blob/main/docs/getting-started.md)
21
+ - [All guides](https://github.com/gemstack-rb/gemstack/tree/main/docs) ·
22
+ [Architecture](https://github.com/gemstack-rb/gemstack/blob/main/ARCHITECTURE.md)
23
+
24
+ Source, issues and pull requests: [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack)
25
+ (this gem lives in `gems/gemstack`).
26
+
27
+ ## License
28
+
29
+ MIT — see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ module GemStack
6
+ # A GemStack application: configuration + autoloaded code + routes, served
7
+ # as a Rack app. There is one per process, available as GemStack.application.
8
+ #
9
+ # Boot sequence (idempotent):
10
+ # 1. load .env files (development/test) config.env_files
11
+ # 2. load config/environments/<env>.rb if present
12
+ # 3. run plugin hooks GemStack::Plugins
13
+ # 4. set up Zeitwerk for every directory in app/ (+ config.autoload_paths)
14
+ # 5. load config/routes.rb
15
+ # 6. compile the middleware stack config.http.middleware
16
+ # 7. eager load in production config.eager_load
17
+ class Application
18
+ attr_reader :config, :loader, :router
19
+
20
+ def initialize(config: GemStack.config)
21
+ @config = config
22
+ @booted = false
23
+ @boot_lock = Mutex.new
24
+ @shutdown_hooks = []
25
+ @reload_hooks = []
26
+ end
27
+
28
+ def root = Pathname.new(config.root)
29
+
30
+ def boot!
31
+ @boot_lock.synchronize do
32
+ return self if @booted
33
+
34
+ load_env_files
35
+ load_environment_config
36
+ enable_jit
37
+ Plugins.run(self)
38
+ setup_loader
39
+ @router = build_router { load_routes }
40
+ install_reloader if config.reload_code
41
+ @http = HTTP::App.new(config: config.http, router: @router)
42
+ eager_load! if config.eager_load
43
+ @booted = true
44
+ end
45
+ self
46
+ end
47
+
48
+ def booted? = @booted
49
+
50
+ def call(env)
51
+ boot! unless @booted
52
+ @http.call(env)
53
+ end
54
+
55
+ # Replaces the route table. Called by config/routes.rb via GemStack.routes.
56
+ def draw_routes(&)
57
+ raise ArgumentError, "GemStack.routes needs a block" unless block_given?
58
+
59
+ @pending_routes << Proc.new(&) if @pending_routes # collected while (re)loading routes.rb
60
+ end
61
+
62
+ def routes = @router&.routes || []
63
+
64
+ # Unloads and reloads app/ code and routes. Used by the development
65
+ # Reloader; safe to call from the console.
66
+ def reload!
67
+ unless config.reload_code
68
+ raise ConfigurationError, "code reloading is disabled; set config.reload_code = true (default in development)"
69
+ end
70
+
71
+ @loader&.reload
72
+ @router = build_router { load_routes }
73
+ @http.router = @router
74
+ @reload_hooks.each(&:call)
75
+ GemStack.logger.debug("reloaded application code")
76
+ true
77
+ end
78
+
79
+ def eager_load!
80
+ @loader&.eager_load
81
+ end
82
+
83
+ # Runs after code and routes reload (development), e.g. to re-read config/channels.rb.
84
+ def on_reload(&block)
85
+ @reload_hooks << block
86
+ end
87
+
88
+ def on_shutdown(&block)
89
+ @shutdown_hooks << block
90
+ end
91
+
92
+ def shutdown
93
+ @shutdown_hooks.reverse_each(&:call)
94
+ end
95
+
96
+ def autoload_dirs
97
+ app_dirs = Dir.glob(root.join("app/*").to_s)
98
+ extra = Array(config.autoload_paths).map { |path| File.expand_path(path, root) }
99
+ (app_dirs + extra).select { |path| File.directory?(path) }
100
+ end
101
+
102
+ private
103
+
104
+ def load_env_files = GemStack.load_env_files!
105
+
106
+ # Enables config.jit unless a JIT is already running (e.g. `ruby --yjit`).
107
+ def enable_jit
108
+ jit = config.jit
109
+ return unless jit
110
+
111
+ engine = { yjit: :YJIT, zjit: :ZJIT }.fetch(jit.to_sym) do
112
+ raise ConfigurationError, "unknown config.jit #{jit.inspect} (use :yjit, :zjit or nil)"
113
+ end
114
+ return unless RubyVM.const_defined?(engine)
115
+ return if %i[YJIT ZJIT].any? { |name| RubyVM.const_defined?(name) && RubyVM.const_get(name).enabled? }
116
+
117
+ RubyVM.const_get(engine).enable
118
+ end
119
+
120
+ def load_environment_config
121
+ file = root.join("config/environments/#{GemStack.env}.rb")
122
+ load file.to_s if file.file?
123
+ end
124
+
125
+ def setup_loader
126
+ @loader = Zeitwerk::Loader.new
127
+ @loader.tag = "gemstack.app"
128
+ autoload_dirs.each { |dir| @loader.push_dir(dir) }
129
+ @loader.enable_reloading if config.reload_code
130
+ @loader.setup
131
+ end
132
+
133
+ # Inside the ErrorHandler, so errors raised while reloading (e.g. a
134
+ # SyntaxError) are rendered as JSON error responses.
135
+ def install_reloader
136
+ stack = config.http.middleware
137
+ return if stack.include?(Reloader)
138
+
139
+ if stack.include?(HTTP::Middleware::ErrorHandler)
140
+ stack.insert_after(HTTP::Middleware::ErrorHandler, Reloader, self)
141
+ else
142
+ stack.unshift(Reloader, self)
143
+ end
144
+ end
145
+
146
+ def load_routes
147
+ file = root.join("config/routes.rb")
148
+ load file.to_s if file.file?
149
+ end
150
+
151
+ def build_router
152
+ @pending_routes = []
153
+ yield
154
+ router = HTTP::Router.new(prefix: config.http.api_path)
155
+ @pending_routes.each { |block| router.draw(&block) }
156
+ router
157
+ ensure
158
+ @pending_routes = nil
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemStack
4
+ # A readers/writer lock. Requests hold a shared lock while application code
5
+ # runs; code reloading takes the exclusive lock, so a reload never happens
6
+ # while another thread is executing code that is about to be unloaded.
7
+ class Interlock
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @condition = ConditionVariable.new
11
+ @readers = 0
12
+ @writer = false
13
+ @waiting_writers = 0
14
+ end
15
+
16
+ def acquire_shared
17
+ @mutex.synchronize do
18
+ # Waiting writers get priority so a busy server can't starve a reload.
19
+ @condition.wait(@mutex) while @writer || @waiting_writers.positive?
20
+ @readers += 1
21
+ end
22
+ end
23
+
24
+ def release_shared
25
+ @mutex.synchronize do
26
+ @readers -= 1
27
+ @condition.broadcast if @readers.zero?
28
+ end
29
+ end
30
+
31
+ def exclusive
32
+ @mutex.synchronize do
33
+ @waiting_writers += 1
34
+ @condition.wait(@mutex) while @writer || @readers.positive?
35
+ @waiting_writers -= 1
36
+ @writer = true
37
+ end
38
+ yield
39
+ ensure
40
+ @mutex.synchronize do
41
+ @writer = false
42
+ @condition.broadcast
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gemstack/dev/file_watcher"
4
+
5
+ module GemStack
6
+ # Development middleware: reloads application code and routes before a
7
+ # request when files under app/ or config/routes.rb have changed
8
+ # (DECISIONS D-007). The shared lock is held until the response body is
9
+ # closed, so streamed responses finish on the code that produced them.
10
+ class Reloader
11
+ WATCHED = ["app/**/*", "config/routes.rb"].freeze
12
+
13
+ def initialize(app, application)
14
+ @app = app
15
+ @application = application
16
+ @watcher = Dev::FileWatcher.new(WATCHED, root: application.root)
17
+ @interlock = Interlock.new
18
+ @check = Mutex.new
19
+ end
20
+
21
+ def call(env)
22
+ reload_if_changed
23
+ @interlock.acquire_shared
24
+ begin
25
+ status, headers, body = @app.call(env)
26
+ rescue Exception # rubocop:disable Lint/RescueException -- release the lock, then re-raise anything
27
+ @interlock.release_shared
28
+ raise
29
+ end
30
+ [status, headers, Rack::BodyProxy.new(body) { @interlock.release_shared }]
31
+ end
32
+
33
+ private
34
+
35
+ def reload_if_changed
36
+ changed = @check.synchronize { @watcher.changed? }
37
+ return unless changed
38
+
39
+ @interlock.exclusive { @application.reload! }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "rack/test"
5
+ require "json"
6
+
7
+ module GemStack
8
+ # Test helpers for GemStack applications (Minitest + rack-test).
9
+ #
10
+ # class ProductsTest < GemStack::TestCase
11
+ # def test_index
12
+ # get_json "/api/products"
13
+ # assert_status 200
14
+ # assert_equal [], json_body
15
+ # end
16
+ # end
17
+ module Testing
18
+ module Helpers
19
+ include Rack::Test::Methods
20
+
21
+ def app = GemStack.application
22
+
23
+ # Parsed JSON body of the last response.
24
+ def json_body
25
+ JSON.parse(last_response.body)
26
+ end
27
+
28
+ def get_json(path, params = {}, headers = {})
29
+ get(path, params, json_headers(headers))
30
+ end
31
+
32
+ %w[post put patch delete].each do |verb|
33
+ define_method(:"#{verb}_json") do |path, body = {}, headers = {}|
34
+ public_send(verb, path, JSON.generate(body),
35
+ json_headers(headers).merge("CONTENT_TYPE" => "application/json"))
36
+ end
37
+ end
38
+
39
+ def assert_status(expected, message = nil)
40
+ expected = Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(expected) if expected.is_a?(Symbol)
41
+ message ||= "Expected status #{expected}, got #{last_response.status}: #{last_response.body[0, 500]}"
42
+ assert_equal expected, last_response.status, message
43
+ end
44
+
45
+ # assert_error 422, "validation_failed"
46
+ def assert_error(status, code = nil)
47
+ assert_status status
48
+ assert_equal code, json_body.dig("error", "code") if code
49
+ end
50
+
51
+ private
52
+
53
+ def json_headers(headers) = { "HTTP_ACCEPT" => "application/json" }.merge(headers)
54
+ end
55
+ end
56
+
57
+ class TestCase < Minitest::Test
58
+ include Testing::Helpers
59
+ end
60
+ end
data/lib/gemstack.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gemstack/core"
4
+ require "gemstack/cache"
5
+ require "gemstack/http"
6
+ require "gemstack/dev"
7
+ require "gemstack/contract" # /api/docs in development; `gemstack contract`
8
+ require_relative "gemstack/interlock"
9
+ require_relative "gemstack/reloader"
10
+ require_relative "gemstack/application"
11
+
12
+ module GemStack
13
+ class Config
14
+ # Reload app/ code and routes between requests when files change.
15
+ setting :reload_code, default: -> { GemStack.env.development? }
16
+ # Load all application code at boot (faster first requests, copy-on-write
17
+ # friendly with Puma workers).
18
+ setting :eager_load, default: -> { !GemStack.env.local? }
19
+ # Extra directories to autoload, relative to the root (e.g. ["lib"]).
20
+ setting :autoload_paths, default: []
21
+ # Ruby JIT enabled at boot: :yjit (production default, measured fastest),
22
+ # :zjit (Ruby 4's newer JIT, opt-in), or nil. GEMSTACK_JIT=yjit|zjit|off overrides.
23
+ setting :jit, default: lambda {
24
+ case ENV.fetch("GEMSTACK_JIT", nil)
25
+ when "off", "none", "" then nil
26
+ when String then ENV["GEMSTACK_JIT"].to_sym
27
+ else GemStack.env.production? ? :yjit : nil
28
+ end
29
+ }
30
+ end
31
+
32
+ # Short names for application code:
33
+ # class ApplicationController < GemStack::Controller
34
+ Controller = HTTP::Controller
35
+ Params = HTTP::Params
36
+ # GemStack::Schema, GemStack::Serializer and GemStack::Types come from gemstack-schema.
37
+
38
+ class << self
39
+ def application
40
+ @application ||= Application.new(config: config)
41
+ end
42
+
43
+ attr_writer :application
44
+
45
+ def boot! = application.boot!
46
+
47
+ # config/routes.rb:
48
+ # GemStack.routes do
49
+ # resources :products
50
+ # end
51
+ def routes(&)
52
+ return application.routes unless block_given?
53
+
54
+ application.draw_routes(&)
55
+ end
56
+
57
+ alias reset_core! reset!
58
+
59
+ def reset!
60
+ reset_core!
61
+ @application = nil
62
+ end
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gemstack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shoaib Malik
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gemstack-cache
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: gemstack-cli
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: gemstack-contract
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.1.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: gemstack-core
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 0.1.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 0.1.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: gemstack-dev
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '='
73
+ - !ruby/object:Gem::Version
74
+ version: 0.1.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 0.1.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: gemstack-http
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '='
87
+ - !ruby/object:Gem::Version
88
+ version: 0.1.0
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '='
94
+ - !ruby/object:Gem::Version
95
+ version: 0.1.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: gemstack-schema
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '='
101
+ - !ruby/object:Gem::Version
102
+ version: 0.1.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: 0.1.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: puma
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '6.4'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '6.4'
124
+ - !ruby/object:Gem::Dependency
125
+ name: zeitwerk
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.6'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2.6'
138
+ email:
139
+ - gemstack26@gmail.com
140
+ executables: []
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - CHANGELOG.md
145
+ - LICENSE.txt
146
+ - README.md
147
+ - lib/gemstack.rb
148
+ - lib/gemstack/application.rb
149
+ - lib/gemstack/interlock.rb
150
+ - lib/gemstack/reloader.rb
151
+ - lib/gemstack/testing.rb
152
+ homepage: https://github.com/gemstack-rb/gemstack
153
+ licenses:
154
+ - MIT
155
+ metadata:
156
+ rubygems_mfa_required: 'true'
157
+ source_code_uri: https://github.com/gemstack-rb/gemstack/tree/main/gems/gemstack
158
+ changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/gems/gemstack/CHANGELOG.md
159
+ bug_tracker_uri: https://github.com/gemstack-rb/gemstack/issues
160
+ documentation_uri: https://github.com/gemstack-rb/gemstack/tree/main/docs
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '4.0'
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubygems_version: 4.0.20
176
+ specification_version: 4
177
+ summary: 'GemStack: a fast, modular Ruby API framework for Next.js applications'
178
+ test_files: []