eks-cent 3.0.0 โ†’ 4.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67ef903d9f6fc46bb7f71450367a2fb7f8e7c278467e1124c33944ee1799486b
4
- data.tar.gz: 602f77e111762d9b6a9713e5fd5547cd1dce06c972abedafa22db9b3e88fe7c9
3
+ metadata.gz: 2ed494c921a2bb36933450d69a731cc504340a404d3b101224a5247b81f99666
4
+ data.tar.gz: 8f99a004701ac50d66be5db2385dfb01ad57860eb78ce67ec38d1dbe5be2d54a
5
5
  SHA512:
6
- metadata.gz: 035074f92c6cca99e6ae40dbdc8e526ea3498f2a4cc4b535b5a3c8f8311c2b11cd4c76d4fec8e05e3924ad8909867f273d26964fbd81bf28783f43fbe8fdf1af
7
- data.tar.gz: 010e6e4519f6a5d50231aa457f2d0fc1ec4f655788247179b8b94d4c739507007148fcb0f0c5a253c903b45af667a81e0671f76634fff807bb6fd4cb1b6b85a0
6
+ metadata.gz: babec8d09352ead4578f627d0e9cd69d57d234e6e2d3d6f7471884ec424354a84270ae8ad80b10f855862d973517ae8da4aaa4d59dfdb9d6d8194a3fbbe5d983
7
+ data.tar.gz: d77626acdc60cb91342839ac4c73c533adaa6b4d37c38c581ba23f75e5e22ea0b8246fb1e850f68ad6bc92a689573d0da272869f65199cacc6cad5f0804ec226
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog - Eks-Cent
2
+
3
+ Semua perubahan penting pada proyek ini akan didokumentasikan dalam file ini.
4
+
5
+ ## [4.0.0] - 2026-03-31
6
+
7
+ Rilis Major dengan peningkatan arsitektural signifikan dan fitur standar Eks Interface.
8
+
9
+ ### Ditambahkan
10
+ - **URL Mapping (`map`)**: Memungkinkan menjalankan beberapa aplikasi di bawah jalur URL yang berbeda.
11
+ - **Application Cascade**: Mekanisme fallback otomatis antar aplikasi jika terjadi 404.
12
+ - **Middleware Baru**:
13
+ - `EksCent::Middleware::Runtime`: Header `X-Runtime` untuk pelacakan performa.
14
+ - `EksCent::Middleware::MethodOverride`: Dukungan method HTTP non-GET/POST via parameter `_method`.
15
+ - `EksCent::Middleware::Head`: Penanganan otomatis permintaan `HEAD`.
16
+ - **Keamanan**: Batasan parameter (`EKS_QUERY_PARSER_PARAMS_LIMIT` dan `EKS_MULTIPART_TOTAL_PART_LIMIT`) untuk mitigasi serangan DoS.
17
+ - **Helper Respons**: Menambahkan metode `set_header` dan `content_type=` pada kelas `Response`.
18
+ - **Mock Testing**: Objek `MockResponse` yang lebih kaya fitur untuk pengujian unit.
19
+
20
+ ### Diubah
21
+ - **Response Layout**: Sistem layout ERB kini mendeteksi `views/layout.erb` secara otomatis.
22
+ - **Error Handling**: Peningkatan UI pada middleware `ShowExceptions`.
23
+ - **CLI**: Perbaikan logika auto-reload pada `ekscentup -R`.
24
+
25
+ ---
26
+
27
+ ## [1.0.0] - [3.0.0] - 2026-03-28
28
+
29
+ Peningkatan stabilitas dan fitur pendukung rute.
30
+
31
+ ### Ditambahkan
32
+ - Dukungan `multipart/form-data` menggunakan `Eks Standard Request` secara internal.
33
+ - Mekanisme `halt` di Router menggunakan `catch/throw`.
34
+ - Custom `not_found` dan `error` DSL di Router.
35
+ - Injeksi objek `@req` dan `@res` ke dalam template ERB.
data/README.md CHANGED
@@ -1,106 +1,188 @@
1
- # Eks-Cent Framework ๐Ÿš€
1
+ <p align="center">
2
+ <img src="images/logo.webp" alt="Eks-Cent Logo" width="200" />
3
+ </p>
2
4
 
3
- **Eks-Cent** adalah framework web Ruby ringan yang terinspirasi oleh Rack. Dirancang untuk kecepatan, keamanan, dan kemudahan penggunaan tanpa dependensi eksternal yang berat.
5
+ # Eks-Cent Framework v4.0.0 ๐Ÿš€
4
6
 
5
- Eks-Cent menggunakan **Eksa-Server** sebagai engine server bawaan untuk performa tinggi di lingkungan produksi.
7
+ **Eks-Cent** adalah framework web Ruby modern yang ringan, menggunakan standar **Eks Interface**. Dirancang untuk kecepatan, keamanan tingkat tinggi, dan fleksibilitas tanpa beban dependensi eksternal yang besar. Dengan v4.0.0, Eks-Cent kini mendukung fitur arsitektural canggih seperti **URL Mapping** dan **Application Cascading**.
6
8
 
7
- ## โœจ Fitur Utama
9
+ ---
8
10
 
9
- - ๐Ÿ›ค **Advanced Routing**: DSL rute yang bersih dengan dukungan parameter dinamis dan namespace.
10
- - ๐Ÿ” **Security Pack**:
11
- - **Signed Sessions**: Data session aman dengan HMAC-SHA256.
12
- - **XSS Protection**: Helper `h` otomatis untuk escape HTML di template.
13
- - **Security Headers**: Middleware bawaan untuk header Frame-Options, XSS-Protection, dll.
14
- - ๐ŸŽจ **Templating**: Integrasi native dengan **ERB** (Embedded Ruby).
15
- - ๐Ÿ“ฆ **JSON Ready**: Parsing otomatis untuk request body `application/json`.
16
- - โšก **High Performance**: Terintegrasi dengan Eksa-Server (Cluster mode & Workers).
17
- - ๐Ÿ›  **CLI Tool**: Jalankan aplikasi dengan perintah `ekscentup` layaknya `rackup`.
11
+ ## โœจ Fitur Utama v4.0.0
18
12
 
19
- ## ๐Ÿ“ฆ Instalasi
13
+ - ๐Ÿ›ค **Modern Routing DSL**: Pendefinisian rute yang intuitif dengan parameter dinamis (`:name`), namespace, dan kontrol eksekusi (`halt`).
14
+ - ๐Ÿ—บ **URL Mapping & Cascading**: Jalankan beberapa aplikasi independen di bawah satu server berdasarkan sub-jalur (path mapping) atau fallback otomatis.
15
+ - ๐Ÿ” **Security First**: Session terenkripsi HMAC-SHA256, proteksi XSS otomatis, header keamanan bawaan, dan pembatasan parameter (DoS protection).
16
+ - ๐Ÿ›  **Standard Middleware Suite**:
17
+ - `Runtime`: Pantau performa dengan header `X-Runtime`.
18
+ - `MethodOverride`: Gunakan `PUT/DELETE` dari form HTML biasa.
19
+ - `Head`: Otomatisasi penanganan request `HEAD`.
20
+ - `Session`, `Logger`, `Static`, `ShowExceptions`, dll.
21
+ - ๐ŸŽจ **Smart Templating**: Integrasi **ERB** dengan sistem **Auto-Layout**, injeksi objek `@req`/`@res`, dan helper keamanan `h`.
22
+ - ๐Ÿงช **First-Class Testing**: Infrastruktur pengujian bawaan menggunakan `MockRequest` dan `MockResponse`.
23
+ - โšก **Production Ready**: Terintegrasi penuh dengan **Eksa-Server** (Cluster mode & Workers) dan CLI tool `ekscentup`.
20
24
 
21
- Tambahkan baris ini ke dalam Gemfile aplikasi Anda:
25
+ ---
22
26
 
23
- ```ruby
24
- gem 'eks-cent'
25
- ```
26
-
27
- Lalu jalankan:
28
- ```bash
29
- gem install eks-cent
30
- ```
31
-
32
- ## ๐Ÿš€ Memulai Cepat
27
+ ## ๐Ÿš€ Memulai Cepat (v4 Style)
33
28
 
34
- Buat file bernama `config.eks`:
29
+ Buat file `config.eks` untuk mendefinisikan aplikasi Anda:
35
30
 
36
31
  ```ruby
37
- # config.eks
38
- use EksCent::Middleware::ContentSecurity
32
+ # 1. Global Middlewares
33
+ use EksCent::Middleware::Runtime
34
+ use EksCent::Middleware::MethodOverride
39
35
  use EksCent::Middleware::Session
40
36
  use EksCent::Middleware::Logger
41
- use EksCent::Middleware::ShowExceptions
42
37
 
38
+ # 2. Pemetaan Aplikasi API
39
+ map "/api" do
40
+ api = EksCent::Router.new do
41
+ get '/status' do |req, res|
42
+ res.content_type = 'application/json'
43
+ res.write({ status: 'online', version: EksCent::VERSION }.to_json)
44
+ end
45
+ end
46
+ run api
47
+ end
48
+
49
+ # 3. Aplikasi Web Utama
43
50
  router = EksCent::Router.new do
44
- # Halaman Utama
45
51
  get '/' do |req, res|
46
52
  req.session['visits'] ||= 0
47
53
  req.session['visits'] += 1
48
- res.write "<h1>Halo! Anda sudah berkunjung #{req.session['visits']} kali.</h1>"
54
+ res.write "<h1>Web Utama v#{EksCent::VERSION}</h1>"
55
+ res.write "<p>Kunjungan Anda: #{req.session['visits']}</p>"
49
56
  end
57
+ end
50
58
 
51
- # Rute Dinamis
52
- get '/hello/:name' do |req, res|
53
- res.render 'welcome', name: req.params['name']
59
+ run router
60
+ ```
61
+
62
+ Jalankan dengan perintah:
63
+ ```bash
64
+ ekscentup -R --port 3000
65
+ ```
66
+
67
+ ---
68
+
69
+ ## ๐Ÿ›ค Dokumentasi Routing
70
+
71
+ ### Router DSL
72
+ Anda dapat mendefinisikan rute menggunakan metode HTTP standar (`get`, `post`, `put`, `delete`, `patch`, `options`, `any`).
73
+
74
+ ```ruby
75
+ router = EksCent::Router.new do
76
+ get '/user/:id' do |req, res|
77
+ id = req.params['id']
78
+ res.write "User ID: #{id}"
54
79
  end
55
80
 
56
- # API JSON
57
- namespace '/api' do
58
- post '/data' do |req, res|
59
- res.headers['Content-Type'] = 'application/json'
60
- res.write({ status: 'success', data: req.params }.to_json)
81
+ namespace '/admin' do
82
+ get '/dashboard' do |req, res|
83
+ # Diakses via /admin/dashboard
61
84
  end
62
85
  end
86
+
87
+ # Kontrol Eksekusi
88
+ get '/secret' do |req, res|
89
+ halt(403, "Akses ditolak") unless req.session['admin']
90
+ res.write "Data Rahasia"
91
+ end
63
92
  end
93
+ ```
64
94
 
65
- run router
95
+ ### URL Mapping & Cascade
96
+ Gunakan `map` untuk membagi aplikasi besar menjadi sub-aplikasi yang lebih kecil. Gunakan `cascade` jika Anda ingin mencoba beberapa aplikasi secara bergantian hingga ada yang merespons (selain 404).
97
+
98
+ ---
99
+
100
+ ## ๐Ÿ“ฆ Middleware Bawaan
101
+
102
+ | Middleware | Deskripsi |
103
+ |------------|-----------|
104
+ | `EksCent::Middleware::Runtime` | Menambahkan header `X-Runtime` dengan waktu eksekusi. |
105
+ | `EksCent::Middleware::MethodOverride` | Mengizinkan override method HTTP via parameter `_method`. |
106
+ | `EksCent::Middleware::Session` | Manajemen session aman berbasis cookie dengan HMAC. |
107
+ | `EksCent::Middleware::Logger` | Logging permintaan ke STDOUT atau file log. |
108
+ | `EksCent::Middleware::Static` | Melayani file statis dari direktori tertentu (misal: `public`). |
109
+ | `EksCent::Middleware::ShowExceptions` | Menampilkan halaman error yang informatif saat terjadi *crash*. |
110
+ | `EksCent::Middleware::ContentSecurity` | Menambahkan header keamanan standar (X-Content-Type, X-Frame-Options). |
111
+ | `EksCent::Middleware::Head` | Mengosongkan body untuk request HEAD secara otomatis. |
112
+
113
+ ---
114
+
115
+ ## ๐ŸŽจ Templating & Layout
116
+
117
+ Letakkan file `.erb` Anda di dalam direktori `views/`. Secara otomatis, framework akan mencari `views/layout.erb` sebagai pembungkus utama.
118
+
119
+ **views/layout.erb**:
120
+ ```erb
121
+ <html>
122
+ <body>
123
+ <header>My App</header>
124
+ <%= @content %> <!-- Konten dari render akan disisipkan di sini -->
125
+ </body>
126
+ </html>
66
127
  ```
67
128
 
68
- Jalankan server:
69
- ```bash
70
- ./bin/ekscentup -p 3000
129
+ Di dalam Router:
130
+ ```ruby
131
+ res.render 'index', judul: "Halo Dunia"
71
132
  ```
72
133
 
73
- ## ๐Ÿ›  Penggunaan CLI (`ekscentup`)
134
+ **Context Injection**: Objek `@req` (request) dan `@res` (response) serta helper `@h` (escape HTML) selalu tersedia di dalam template.
74
135
 
75
- | Opsi | Deskripsi |
76
- |------|-----------|
77
- | `-p, --port` | Menentukan port server (default: 3000) |
78
- | `-o, --host` | Menentukan host server (default: 0.0.0.0) |
79
- | `-w, --workers` | Jumlah worker untuk mode Cluster |
80
- | `-R, --reload` | Aktifkan auto-reload saat file berubah |
81
- | `-e, --env` | Set lingkungan (`development` atau `production`) |
82
- | `-L, --log` | Simpan log ke file tertentu |
136
+ ---
83
137
 
84
- ## ๐Ÿ›ก Mode Produksi
138
+ ## ๐Ÿ›  Panduan API (Request & Response)
85
139
 
86
- Untuk keamanan maksimal di produksi, pastikan Anda menyetel environment variable dan secret key:
140
+ ### EksCent::Request (`req`)
141
+ - `req.params`: Mengambil parameter query, POST, atau route params.
142
+ - `req.session`: Mengakses data session (Read/Write).
143
+ - `req.request_method`: Mendapatkan method HTTP (GET, POST, dll).
144
+ - `req.path`: Mendapatkan jalur URL saat ini.
145
+ - `req.user_agent`: Mendapatkan informasi browser.
87
146
 
88
- ```bash
89
- export EKS_CENT_SECRET_KEY_BASE="kunci_rahasia_anda_yang_unik"
90
- export RACK_ENV=production
91
- ./bin/ekscentup -p 80 -e production
92
- ```
147
+ ### EksCent::Response (`res`)
148
+ - `res.write(string)`: Menambahkan konten ke body respons.
149
+ - `res.set_header(key, value)`: Mengatur header HTTP.
150
+ - `res.content_type = 'type'`: Shortcut untuk mengatur Content-Type.
151
+ - `res.status = code`: Mengatur status code (default: 200).
152
+ - `res.redirect(path)`: Melakukan pengalihan URL.
153
+ - `res.render(template, locals)`: Merender template ERB.
93
154
 
94
- ## ๐Ÿงช Pengujian
155
+ ---
95
156
 
96
- Eks-Cent dilengkapi dengan suite pengujian yang lengkap. Untuk menjalankan semua tes:
157
+ ## ๐Ÿงช Pengujian (Testing)
97
158
 
98
- ```bash
99
- ruby test/eks_cent_test.rb
100
- ruby test/router_test.rb
101
- ruby test/security_test.rb
159
+ Gunakan suite pengujian bawaan untuk memastikan aplikasi Anda berjalan dengan benar:
160
+
161
+ ```ruby
162
+ require 'test/unit'
163
+ require 'eks-cent'
164
+
165
+ class MyAppTest < Test::Unit::TestCase
166
+ def test_homepage
167
+ app = EksCent.load('config.eks')
168
+ mock = EksCent::MockRequest.new(app)
169
+
170
+ res = mock.get('/')
171
+ assert res.ok?
172
+ assert_match "Welcome", res.body_content
173
+ end
174
+ end
102
175
  ```
103
176
 
104
- ## ๐Ÿ“„ Lisensi
177
+ ---
178
+
179
+ ## ๐Ÿ›ก Keamanan Dasar (Eks Limits)
180
+ Anda dapat mengatur batasan penguraian parameter melalui variabel lingkungan untuk mencegah serangan DoS:
181
+ - `EKS_QUERY_PARSER_PARAMS_LIMIT`: Maksimal jumlah parameter (default: 1000).
182
+ - `EKS_QUERY_PARSER_DEPTH_LIMIT`: Maksimal kedalaman parameter nested.
183
+ - `EKS_MULTIPART_TOTAL_PART_LIMIT`: Maksimal part dalam form multipart.
105
184
 
106
- Eks-Cent didistribusikan di bawah [Lisensi MIT](LICENSE).
185
+ ---
186
+
187
+ ## ๐Ÿ“„ Lisensi
188
+ Eks-Cent v4.0.0 dipublikasikan di bawah [Lisensi MIT](LICENSE).
data/bin/ekscentup CHANGED
@@ -22,7 +22,7 @@ options = {
22
22
  workers: 0,
23
23
  reload: false,
24
24
  timeout: 30,
25
- env: ENV['RACK_ENV'] || 'development'
25
+ env: ENV['EKS_ENV'] || ENV['RACK_ENV'] || 'development'
26
26
  }
27
27
 
28
28
  OptionParser.new do |opts|
@@ -56,7 +56,7 @@ OptionParser.new do |opts|
56
56
 
57
57
  opts.on("-e", "--env ENV", "Set environment (development/production)") do |v|
58
58
  options[:env] = v
59
- ENV['RACK_ENV'] = v
59
+ ENV['EKS_ENV'] = v
60
60
  end
61
61
 
62
62
  opts.on("-v", "--version", "Tampilkan versi Eks-Cent") do
@@ -99,22 +99,86 @@ end
99
99
 
100
100
  puts "\e[34m[Eks-Cent] Environment: #{EksCent.env}\e[0m"
101
101
 
102
- # Load aplikasi via Builder
103
- begin
104
- app = EksCent.load(config_path)
105
- rescue => e
106
- puts "\e[31mGagal memuat aplikasi: #{e.message}\e[0m"
107
- puts e.backtrace.first(5).join("\n")
108
- exit 1
102
+ # Fungsi untuk menjalankan server
103
+ def start_server(config_path, options)
104
+ begin
105
+ # Load aplikasi via Builder di setiap start agar config direfresh
106
+ app = EksCent.load(config_path)
107
+ server = EksaServerCore.new(app, options.merge(reload: false))
108
+ server.start
109
+ rescue Interrupt
110
+ # Biarkan parent menangani Interrupt
111
+ exit 0
112
+ rescue => e
113
+ puts "\e[31m[Eks-Cent] Server error: #{e.message}\e[0m"
114
+ puts e.backtrace.first(5).join("\n")
115
+ exit 1
116
+ end
109
117
  end
110
118
 
111
- # Jalankan menggunakan EksaServerCore
112
- begin
113
- server = EksaServerCore.new(app, options)
114
- server.start
115
- rescue Interrupt
116
- puts "\n\e[33mMenutup server Eks-Cent...\e[0m"
117
- rescue => e
118
- puts "\e[31mServer error: #{e.message}\e[0m"
119
- exit 1
119
+ if options[:reload] && Process.respond_to?(:fork)
120
+ puts "\e[34m[Eks-Cent] Auto-reload aktif. Memantau perubahan file di direktori ini...\e[0m"
121
+
122
+ trap("INT") do
123
+ puts "\n\e[33mMenutup Reloader Eks-Cent...\e[0m"
124
+ Process.kill("INT", @child_pid) rescue nil if @child_pid
125
+ exit 0
126
+ end
127
+
128
+ loop do
129
+ @child_pid = fork do
130
+ start_server(config_path, options)
131
+ end
132
+
133
+ # Pemantau file sederhana (cek mtime setiap detik)
134
+ watched_extensions = %w[.rb .eks .erb .html .css .js]
135
+ initial_mtimes = {}
136
+
137
+ # Inisialisasi daftar file
138
+ Dir.glob("**/*").each do |f|
139
+ next unless watched_extensions.include?(File.extname(f))
140
+ initial_mtimes[f] = File.mtime(f) rescue nil
141
+ end
142
+
143
+ loop do
144
+ sleep 1
145
+
146
+ # Periksa perubahan
147
+ changed = false
148
+ Dir.glob("**/*").each do |f|
149
+ next unless watched_extensions.include?(File.extname(f))
150
+ current_mtime = File.mtime(f) rescue nil
151
+ if current_mtime && (!initial_mtimes[f] || current_mtime > initial_mtimes[f])
152
+ changed = true
153
+ break
154
+ end
155
+ end
156
+
157
+ if changed
158
+ puts "\e[34m[Reloader] Perubahan terdeteksi. Me-restart server...\e[0m"
159
+ Process.kill("INT", @child_pid) rescue nil
160
+ Process.wait(@child_pid) rescue nil
161
+ break # Keluar ke loop untuk fork baru
162
+ end
163
+
164
+ # Cek jika proses anak berhenti mendadak
165
+ if (pid = Process.waitpid(@child_pid, Process::WNOHANG))
166
+ puts "\e[31m[Reloader] Server (PID: #{pid}) berhenti. Me-restart dalam 2 detik...\e[0m"
167
+ sleep 2
168
+ break
169
+ end
170
+ end
171
+ end
172
+ else
173
+ # Mode Standar (Tanpa Reload)
174
+ begin
175
+ app = EksCent.load(config_path)
176
+ server = EksaServerCore.new(app, options)
177
+ server.start
178
+ rescue Interrupt
179
+ puts "\n\e[33mMenutup server Eks-Cent...\e[0m"
180
+ rescue => e
181
+ puts "\e[31mServer error: #{e.message}\e[0m"
182
+ exit 1
183
+ end
120
184
  end
data/config.eks CHANGED
@@ -1,44 +1,50 @@
1
1
  # File: config.eks
2
- # Contoh konfigurasi untuk Eks-Cent
2
+ # Contoh konfigurasi lanjutan untuk Eks-Cent v4.0.0
3
3
 
4
+ # 1. Globals & Security Middlewares
5
+ use EksCent::Middleware::Runtime # Header X-Runtime otomatis
6
+ use EksCent::Middleware::Head # Penanganan HEAD request otomatis
7
+ use EksCent::Middleware::MethodOverride # Dukungan _method=DELETE/PUT via form
4
8
  use EksCent::Middleware::ContentSecurity
5
9
  use EksCent::Middleware::Session
6
- use EksCent::Middleware::Static, root: 'public'
7
10
  use EksCent::Middleware::Logger
8
11
  use EksCent::Middleware::ShowExceptions
9
12
 
10
- # Aplikasi Utama
11
- # Aplikasi Utama menggunakan Router
12
- router = EksCent::Router.new do
13
+ # 2. Aplikasi API (Dipetakan ke /api)
14
+ map "/api" do
15
+ router_api = EksCent::Router.new do
16
+ get "/status" do |req, res|
17
+ res.set_header 'Content-Type', 'application/json'
18
+ res.write "{\"status\": \"online\", \"version\": \"#{EksCent::VERSION}\"}"
19
+ end
20
+
21
+ put "/update" do |req, res|
22
+ res.write "Method PUT diterima (via MethodOverride). Method asli: #{req.env['eks_cent.original_method']}"
23
+ end
24
+ end
25
+ run router_api
26
+ end
27
+
28
+ # 3. Aplikasi Web Utama (Dipetakan ke root /)
29
+ router_web = EksCent::Router.new do
13
30
  get '/' do |req, res|
14
- # Contoh penggunaan session
15
31
  req.session['visits'] ||= 0
16
32
  req.session['visits'] += 1
17
33
 
18
- res.write "<h1>Selamat Datang di Eks-Cent!</h1>"
19
- res.write "<p>Ini adalah framework tanpa dependensi.</p>"
20
- res.write "<p>Jumlah kunjungan Anda: <strong>#{req.session['visits']}</strong></p>"
21
- res.write "<p>Cobalah akses <a href='/error'>/error</a> untuk melihat middleware ShowExceptions.</p>"
22
- end
23
-
24
- get '/welcome' do |req, res|
25
- # Contoh penggunaan ERB rendering dan h helper
26
- res.render 'test', name: req.params['name'] || 'Tamu'
27
- end
28
-
29
- get '/hello/:name' do |req, res|
30
- name = req.params['name'] || 'Fulan'
31
- res.write "Halo, #{name}! Senang bertemu Anda di #{req.user_agent}."
32
- end
33
-
34
- get '/hello' do |req, res|
35
- name = req.params['name'] || 'Fulan'
36
- res.write "Halo, #{name}! Senang bertemu Anda di #{req.user_agent}."
34
+ res.write "<h1>Selamat Datang di Eks-Cent v#{EksCent::VERSION}!</h1>"
35
+ res.write "<p>Fitur baru: <strong>URL Mapping</strong> & <strong>Runtime Tracking</strong>.</p>"
36
+ res.write "<p>Kunjungan: <strong>#{req.session['visits']}</strong></p>"
37
+ res.write "<hr>"
38
+ res.write "<ul>"
39
+ res.write " <li>Akses API: <a href='/api/status'>/api/status</a></li>"
40
+ res.write " <li>Gunakan MethodOverride: <form action='/api/update' method='post' style='display:inline'><input type='hidden' name='_method' value='PUT'><button type='submit'>Kirim PUT via POST</button></form></li>"
41
+ res.write " <li>Coba Error: <a href='/error'>/error</a></li>"
42
+ res.write "</ul>"
37
43
  end
38
44
 
39
45
  get '/error' do |req, res|
40
- raise "Waduh! Ada kesalahan yang disengaja di sini."
46
+ raise "Kesalahan yang disengaja untuk demonstrasi v4.0.0."
41
47
  end
42
48
  end
43
49
 
44
- run router
50
+ run router_web
data/images/logo.webp ADDED
Binary file
data/lib/eks-cent.rb CHANGED
@@ -1,16 +1,21 @@
1
- # Eks-Cent: Lightweight Rack-like Communication Interface for Ruby.
1
+ # Eks-Cent: Lightweight Eks-style Communication Interface for Ruby.
2
2
 
3
3
  require_relative 'eks_cent/version'
4
4
  require_relative 'eks_cent/request'
5
5
  require_relative 'eks_cent/response'
6
6
  require_relative 'eks_cent/builder'
7
7
  require_relative 'eks_cent/router'
8
+ require_relative 'eks_cent/url_map'
9
+ require_relative 'eks_cent/cascade'
8
10
  require_relative 'eks_cent/mock_request'
9
11
  require_relative 'eks_cent/middleware/logger'
10
12
  require_relative 'eks_cent/middleware/session'
11
13
  require_relative 'eks_cent/middleware/content_security'
12
14
  require_relative 'eks_cent/middleware/show_exceptions'
13
15
  require_relative 'eks_cent/middleware/static'
16
+ require_relative 'eks_cent/middleware/method_override'
17
+ require_relative 'eks_cent/middleware/runtime'
18
+ require_relative 'eks_cent/middleware/head'
14
19
 
15
20
  module EksCent
16
21
 
@@ -22,7 +27,7 @@ module EksCent
22
27
  self.secret_key_base = ENV['EKS_CENT_SECRET_KEY_BASE'] || '1e8a93e80c85b1a6c4b69d9c2e8b2a1a8e1b1d8c1c2e1f2g1h1i1j1k1l1m1n1o'
23
28
 
24
29
  def self.env
25
- ENV['RACK_ENV'] || ENV['EKS_CENT_ENV'] || 'development'
30
+ ENV['EKS_ENV'] || ENV['RACK_ENV'] || ENV['EKS_CENT_ENV'] || 'development'
26
31
  end
27
32
 
28
33
  def self.production?
@@ -14,7 +14,23 @@ module EksCent
14
14
  @run = app
15
15
  end
16
16
 
17
+ def map(path, &block)
18
+ @map ||= {}
19
+ @map[path] = self.class.new(&block).to_app
20
+ end
21
+
22
+ def cascade(*apps)
23
+ require_relative 'cascade' unless defined?(EksCent::Cascade)
24
+ @run = Cascade.new(apps)
25
+ end
26
+
17
27
  def to_app
28
+ if @map
29
+ require_relative 'url_map' unless defined?(EksCent::URLMap)
30
+ @map['/'] = @run if @run
31
+ @run = URLMap.new(@map)
32
+ end
33
+
18
34
  app = @run
19
35
  raise "Aplikasi tidak ditemukan (run nil)" unless app
20
36
  @use.reverse_each { |middleware| app = middleware.call(app) }
@@ -28,7 +44,7 @@ module EksCent
28
44
  builder.to_app
29
45
  end
30
46
 
31
- # Helper for Rack-like interface call
47
+ # Helper for Eks-style interface call
32
48
  def call(env)
33
49
  to_app.call(env)
34
50
  end
@@ -0,0 +1,24 @@
1
+ module EksCent
2
+ class Cascade
3
+ def initialize(apps = [])
4
+ @apps = apps
5
+ end
6
+
7
+ def call(env)
8
+ last_response = [404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
9
+
10
+ @apps.each do |app|
11
+ status, headers, body = app.call(env)
12
+
13
+ # Jika bukan 404, atau header X-Cascade tidak bernilai 'pass', kembalikan respons ini
14
+ if status.to_i != 404 && headers['X-Cascade'] != 'pass'
15
+ return [status, headers, body]
16
+ end
17
+
18
+ last_response = [status, headers, body]
19
+ end
20
+
21
+ last_response
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module EksCent
2
+ module Middleware
3
+ class Head
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ status, headers, body = @app.call(env)
10
+
11
+ if env['REQUEST_METHOD'] == 'HEAD'
12
+ body = []
13
+ end
14
+
15
+ [status, headers, body]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module EksCent
2
+ module Middleware
3
+ class MethodOverride
4
+ HTTP_METHODS = %w(GET HEAD POST PUT DELETE PATCH OPTIONS LINK UNLINK)
5
+ METHOD_OVERRIDE_PARAM_KEY = "_method"
6
+ HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ if env["REQUEST_METHOD"] == "POST"
14
+ method = method_from_env(env)
15
+ if method && HTTP_METHODS.include?(method.upcase)
16
+ env["eks_cent.original_method"] = env["REQUEST_METHOD"]
17
+ env["REQUEST_METHOD"] = method.upcase
18
+ end
19
+ end
20
+
21
+ @app.call(env)
22
+ end
23
+
24
+ private
25
+
26
+ def method_from_env(env)
27
+ # 1. Cek dari header X-HTTP-Method-Override
28
+ return env[HTTP_METHOD_OVERRIDE_HEADER] if env[HTTP_METHOD_OVERRIDE_HEADER]
29
+
30
+ # 2. Cek dari parameter body (_method)
31
+ req = Request.new(env)
32
+ return req.params[METHOD_OVERRIDE_PARAM_KEY] if req.params[METHOD_OVERRIDE_PARAM_KEY]
33
+
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module EksCent
2
+ module Middleware
3
+ class Runtime
4
+ def initialize(app, header_name = 'X-Runtime')
5
+ @app = app
6
+ @header_name = header_name
7
+ end
8
+
9
+ def call(env)
10
+ start_time = Time.now
11
+ status, headers, body = @app.call(env)
12
+ duration = Time.now - start_time
13
+
14
+ headers[@header_name] = format("%0.6f", duration)
15
+ [status, headers, body]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -21,10 +21,16 @@ module EksCent
21
21
  'PATH_INFO' => path,
22
22
  'QUERY_STRING' => query || '',
23
23
  'HTTP_USER_AGENT' => opts[:user_agent] || 'EksCent-MockRequest',
24
- 'rack.input' => StringIO.new(opts[:body] || '')
25
24
  }.merge(opts[:env] || {})
26
25
 
27
- @app.call(env)
26
+ env['eks.input'] ||= StringIO.new(opts[:body] || '')
27
+ env['rack.input'] ||= env['eks.input']
28
+ env['eks.version'] ||= [1, 3]
29
+ env['rack.version'] ||= env['eks.version']
30
+
31
+ status, headers, body = @app.call(env)
32
+ require_relative 'mock_response' unless defined?(EksCent::MockResponse)
33
+ MockResponse.new(status, headers, body)
28
34
  end
29
35
  end
30
36
  end
@@ -0,0 +1,47 @@
1
+ module EksCent
2
+ class MockResponse
3
+ attr_reader :status, :headers, :body
4
+
5
+ def initialize(status, headers, body)
6
+ @status = status.to_i
7
+ @headers = headers
8
+ @body = body
9
+ end
10
+
11
+ def body_content
12
+ @body.is_a?(Array) ? @body.join : @body.to_s
13
+ end
14
+
15
+ def ok?
16
+ @status == 200
17
+ end
18
+
19
+ def redirect?
20
+ [301, 302, 303, 307, 308].include?(@status)
21
+ end
22
+
23
+ def client_error?
24
+ @status >= 400 && @status < 500
25
+ end
26
+
27
+ def server_error?
28
+ @status >= 500 && @status < 600
29
+ end
30
+
31
+ def not_found?
32
+ @status == 404
33
+ end
34
+
35
+ def location
36
+ @headers['Location']
37
+ end
38
+
39
+ def content_type
40
+ @headers['Content-Type']
41
+ end
42
+
43
+ def to_ary
44
+ [@status, @headers, @body]
45
+ end
46
+ end
47
+ end
@@ -8,6 +8,19 @@ module EksCent
8
8
 
9
9
  def initialize(env)
10
10
  @env = env
11
+ # Map standard Rack keys to Eks branding for internal consistency
12
+ @env['eks.input'] ||= @env['rack.input']
13
+ @env['rack.input'] ||= @env['eks.input']
14
+
15
+ @env['eks.version'] ||= @env['rack.version'] || [1, 3]
16
+ @env['rack.version'] ||= @env['eks.version']
17
+
18
+ @env['eks.errors'] ||= @env['rack.errors']
19
+ @env['rack.errors'] ||= @env['eks.errors']
20
+
21
+ @env['eks.multithread'] ||= @env['rack.multithread'] || false
22
+ @env['eks.multiprocess'] ||= @env['rack.multiprocess'] || false
23
+ @env['eks.run_once'] ||= @env['rack.run_once'] || false
11
24
  end
12
25
 
13
26
  def request_method
@@ -56,37 +69,71 @@ module EksCent
56
69
  end
57
70
 
58
71
  def parse_params
59
- params = {}
72
+ require 'rack' unless defined?(Rack)
60
73
 
61
- # Parse query string
62
- params.merge!(CGI.parse(query_string)) if query_string != ''
74
+ # Terapkan batasan global Eks/Rack jika ada di ENV
75
+ setup_eks_limits
76
+
77
+ # Gunakan Rack::Request untuk menangani parsing parameter standar (GET/POST/Multipart)
78
+ rack_req = Rack::Request.new(@env)
79
+ params = rack_req.params.dup
63
80
 
64
- # Parse router params if any
81
+ # Gabungkan dengan parameter dari router (misal: /hello/:name)
65
82
  if @env['eks_cent.router_params']
66
83
  params.merge!(@env['eks_cent.router_params'])
67
84
  end
68
85
 
69
- # Parse body based on content type
70
- if @env['rack.input']
71
- body = @env['rack.input'].read
72
- @env['rack.input'].rewind if @env['rack.input'].respond_to?(:rewind)
73
-
74
- if body && !body.empty?
75
- if @env['CONTENT_TYPE'] == 'application/json'
76
- begin
77
- params.merge!(JSON.parse(body))
78
- rescue JSON::ParserError
79
- # Silently ignore invalid JSON or we could log it
86
+ # Tambahkan parsing JSON manual karena Rack::Request tidak melakukannya secara otomatis
87
+ if @env['CONTENT_TYPE'] == 'application/json' && @env['eks.input']
88
+ begin
89
+ body = @env['eks.input'].read
90
+ @env['eks.input'].rewind if @env['eks.input'].respond_to?(:rewind)
91
+
92
+ if body && !body.empty?
93
+ json_params = JSON.parse(body)
94
+ if json_params.is_a?(Hash)
95
+ # Batasi jumlah parameter JSON jika EKS/RACK_QUERY_PARSER_PARAMS_LIMIT diatur
96
+ limit = (ENV['EKS_QUERY_PARSER_PARAMS_LIMIT'] || ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'])&.to_i || 1000
97
+ if json_params.size > limit
98
+ raise "Too many parameters (JSON)"
99
+ end
100
+ params.merge!(json_params)
80
101
  end
81
- else
82
- # Default to form-urlencoded if it's a POST/PUT/PATCH
83
- params.merge!(CGI.parse(body)) if post? || @env['REQUEST_METHOD'] == 'PUT' || @env['REQUEST_METHOD'] == 'PATCH'
84
102
  end
103
+ rescue JSON::ParserError
104
+ # Abaikan error parsing JSON
85
105
  end
86
106
  end
87
107
 
88
- # Flatten single values
108
+ # Pastikan nilai parameter diratakan (flatten) jika berupa array berukuran 1
109
+ # Catatan: Rack::Request mungkin sudah melakukannya untuk form data standar,
110
+ # tapi kita pastikan konsistensi di sini.
89
111
  params.transform_values { |v| v.is_a?(Array) && v.size == 1 ? v.first : v }
90
112
  end
113
+ def setup_eks_limits
114
+ return if @eks_limits_setup
115
+
116
+ parser = Rack::Utils.default_query_parser rescue nil
117
+ return unless parser
118
+
119
+ params_limit = ENV['EKS_QUERY_PARSER_PARAMS_LIMIT'] || ENV['RACK_QUERY_PARSER_PARAMS_LIMIT']
120
+ if params_limit && parser.respond_to?(:params_limit=)
121
+ parser.params_limit = params_limit.to_i
122
+ end
123
+
124
+ bytesize_limit = ENV['EKS_QUERY_PARSER_BYTESIZE_LIMIT'] || ENV['RACK_QUERY_PARSER_BYTESIZE_LIMIT']
125
+ if bytesize_limit && parser.respond_to?(:bytesize_limit=)
126
+ parser.bytesize_limit = bytesize_limit.to_i
127
+ end
128
+
129
+ multipart_limit = ENV['EKS_MULTIPART_TOTAL_PART_LIMIT'] || ENV['RACK_MULTIPART_TOTAL_PART_LIMIT']
130
+ if multipart_limit
131
+ if Rack::Utils.respond_to?(:multipart_total_part_limit=)
132
+ Rack::Utils.multipart_total_part_limit = multipart_limit.to_i
133
+ end
134
+ end
135
+
136
+ @eks_limits_setup = true
137
+ end
91
138
  end
92
139
  end
@@ -3,18 +3,27 @@ require 'time'
3
3
 
4
4
  module EksCent
5
5
  class Response
6
- attr_accessor :status, :headers, :body
6
+ attr_accessor :status, :headers, :body, :request
7
7
 
8
- def initialize(body = [], status = 200, headers = {})
8
+ def initialize(body = [], status = 200, headers = {}, request: nil)
9
9
  @status = status
10
10
  @headers = { 'Content-Type' => 'text/html' }.merge(headers)
11
11
  @body = body.is_a?(String) ? [body] : body
12
+ @request = request
12
13
  end
13
14
 
14
15
  def write(str)
15
16
  @body << str
16
17
  end
17
18
 
19
+ def set_header(key, value)
20
+ @headers[key] = value
21
+ end
22
+
23
+ def content_type=(type)
24
+ @headers['Content-Type'] = type
25
+ end
26
+
18
27
  def redirect(target, status = 302)
19
28
  @status = status
20
29
  @headers['Location'] = target
@@ -25,7 +34,7 @@ module EksCent
25
34
  @cookies[name] = options.merge(value: value)
26
35
  end
27
36
 
28
- def render(template_name, locals = {})
37
+ def render(template_name, layout: true, **locals)
29
38
  require 'erb'
30
39
  template_path = File.join('views', "#{template_name}.erb")
31
40
  unless File.file?(template_path)
@@ -37,12 +46,34 @@ module EksCent
37
46
  # Gunakan context khusus agar helper h (escape HTML) tersedia
38
47
  context = Object.new
39
48
  context.extend(ERB::Util)
49
+
50
+ # Suntikkan objek internal
51
+ context.instance_variable_set("@req", @request) if @request
52
+ context.instance_variable_set("@res", self)
53
+
54
+ # Suntikkan locals
40
55
  locals.each { |k, v| context.instance_variable_set("@#{k}", v) }
41
56
 
42
- # Definisikan metode helper h secara eksplisit jika perlu
43
- def context.h(s); html_escape(s); end
57
+ # Definisikan metode helper h secara eksplisit untuk keamanan
58
+ def context.h(s); CGI.escapeHTML(s.to_s); end
59
+
60
+ # Render template utama
61
+ result = ERB.new(template_content).result(context.instance_eval { binding })
62
+
63
+ # Dukungan Layout (default mencari views/layout.erb)
64
+ if layout
65
+ layout_name = layout == true ? 'layout' : layout.to_s
66
+ layout_path = File.join('views', "#{layout_name}.erb")
67
+
68
+ if File.file?(layout_path)
69
+ context.instance_variable_set("@content", result)
70
+ layout_content = File.read(layout_path)
71
+ result = ERB.new(layout_content).result(context.instance_eval { binding })
72
+ end
73
+ end
44
74
 
45
- @body << ERB.new(template_content).result(context.instance_eval { binding })
75
+ @headers['Content-Type'] ||= 'text/html'
76
+ @body << result
46
77
  end
47
78
 
48
79
  def finish
@@ -10,9 +10,25 @@ module EksCent
10
10
  }
11
11
  @prefix = ''
12
12
  @current_middlewares = []
13
+ @not_found_block = nil
14
+ @error_block = nil
13
15
  instance_eval(&block) if block_given?
14
16
  end
15
17
 
18
+ def not_found(&block)
19
+ @not_found_block = block
20
+ end
21
+
22
+ def error(&block)
23
+ @error_block = block
24
+ end
25
+
26
+ def halt(res, status = nil, body = nil)
27
+ res.status = status if status
28
+ res.write(body) if body
29
+ throw(:halt)
30
+ end
31
+
16
32
  def get(path, &block) add_route('GET', path, &block) end
17
33
  def post(path, &block) add_route('POST', path, &block) end
18
34
  def put(path, &block) add_route('PUT', path, &block) end
@@ -52,8 +68,19 @@ module EksCent
52
68
  # Wrapped application with route-specific middlewares
53
69
  app = proc do |e|
54
70
  req_i = Request.new(e)
55
- res_i = Response.new
56
- instance_exec(req_i, res_i, &route[:block])
71
+ res_i = Response.new(request: req_i)
72
+ begin
73
+ catch(:halt) do
74
+ instance_exec(req_i, res_i, &route[:block])
75
+ end
76
+ rescue => err
77
+ if @error_block
78
+ res_i.status = 500
79
+ instance_exec(err, req_i, res_i, &@error_block)
80
+ else
81
+ raise err
82
+ end
83
+ end
57
84
  res_i.finish
58
85
  end
59
86
  route[:middlewares].reverse_each { |m| app = m.new(app) }
@@ -61,12 +88,28 @@ module EksCent
61
88
  _status, headers, body = app.call(env)
62
89
  [_status, headers, body]
63
90
  else
64
- [404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
91
+ handle_not_found(env)
65
92
  end
66
93
  end
67
94
 
68
95
  private
69
96
 
97
+ def handle_not_found(env)
98
+ req = Request.new(env)
99
+ res = Response.new(request: req)
100
+ res.status = 404
101
+
102
+ if @not_found_block
103
+ instance_exec(req, res, &@not_found_block)
104
+ res.finish
105
+ elsif File.exist?(File.join('views', '404.erb'))
106
+ res.render '404'
107
+ res.finish
108
+ else
109
+ [404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
110
+ end
111
+ end
112
+
70
113
  def add_route(method, path, &block)
71
114
  keys = []
72
115
  full_path = "#{@prefix}#{path}".gsub('//', '/')
@@ -0,0 +1,29 @@
1
+ module EksCent
2
+ class URLMap
3
+ def initialize(map = {})
4
+ @mapping = map.map do |path, app|
5
+ [path.chomp('/'), app]
6
+ end.sort_by { |path, _| -path.length } # Sort by longest path first
7
+ end
8
+
9
+ def call(env)
10
+ path_info = env['PATH_INFO'] || ''
11
+ script_name = env['SCRIPT_NAME'] || ''
12
+
13
+ @mapping.each do |path, app|
14
+ next unless path_info.start_with?(path)
15
+ next unless path_info == path || path_info[path.length] == '/'
16
+
17
+ # Matched path: shift script_name and path_info
18
+ new_env = env.dup
19
+ new_env['SCRIPT_NAME'] = script_name + path
20
+ new_env['PATH_INFO'] = path_info[path.length..-1].to_s
21
+ new_env['PATH_INFO'] = '/' if new_env['PATH_INFO'].empty?
22
+
23
+ return app.call(new_env)
24
+ end
25
+
26
+ [404, { 'Content-Type' => 'text/plain', 'X-Cascade' => 'pass' }, ["Not Found (URLMap)"]]
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,3 @@
1
1
  module EksCent
2
- VERSION = '3.0.0'
2
+ VERSION = '4.0.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eks-cent
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - IshikawaUta
@@ -27,16 +27,16 @@ dependencies:
27
27
  name: base64
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
32
+ version: '0.2'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '0.2'
40
40
  description: Eks-Cent adalah framework web minimalis yang menyediakan sistem routing
41
41
  canggih, manajemen session HMAC, dan proteksi keamanan bawaan menggunakan Eksa-Server
42
42
  sebagai engine utama.
@@ -47,29 +47,37 @@ executables:
47
47
  extensions: []
48
48
  extra_rdoc_files: []
49
49
  files:
50
+ - CHANGELOG.md
50
51
  - LICENSE
51
52
  - README.md
52
53
  - bin/ekscentup
53
54
  - config.eks
55
+ - images/logo.webp
54
56
  - lib/eks-cent.rb
55
57
  - lib/eks_cent/builder.rb
58
+ - lib/eks_cent/cascade.rb
56
59
  - lib/eks_cent/middleware/content_security.rb
60
+ - lib/eks_cent/middleware/head.rb
57
61
  - lib/eks_cent/middleware/logger.rb
62
+ - lib/eks_cent/middleware/method_override.rb
63
+ - lib/eks_cent/middleware/runtime.rb
58
64
  - lib/eks_cent/middleware/session.rb
59
65
  - lib/eks_cent/middleware/show_exceptions.rb
60
66
  - lib/eks_cent/middleware/static.rb
61
67
  - lib/eks_cent/mock_request.rb
68
+ - lib/eks_cent/mock_response.rb
62
69
  - lib/eks_cent/request.rb
63
70
  - lib/eks_cent/response.rb
64
71
  - lib/eks_cent/router.rb
72
+ - lib/eks_cent/url_map.rb
65
73
  - lib/eks_cent/version.rb
66
74
  homepage: https://github.com/IshikawaUta/eks-cent
67
75
  licenses:
68
76
  - MIT
69
77
  metadata:
70
78
  allowed_push_host: https://rubygems.org
71
- homepage_uri: https://github.com/IshikawaUta/eks-cent
72
79
  source_code_uri: https://github.com/IshikawaUta/eks-cent
80
+ changelog_uri: https://github.com/IshikawaUta/eks-cent/blob/main/CHANGELOG.md
73
81
  rdoc_options: []
74
82
  require_paths:
75
83
  - lib
@@ -86,5 +94,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
94
  requirements: []
87
95
  rubygems_version: 3.6.7
88
96
  specification_version: 4
89
- summary: Framework web Ruby ringan, aman, dan siap produksi berbasis Rack.
97
+ summary: Framework web Ruby ringan, aman, dan siap produksi berbasis Eks Interface.
90
98
  test_files: []