itsi-server 0.2.14 → 0.2.15

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.
@@ -14,12 +14,13 @@ use http::{
14
14
  header::{
15
15
  self, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, LAST_MODIFIED,
16
16
  },
17
- HeaderValue, Response, StatusCode,
17
+ HeaderName, HeaderValue, Response, StatusCode,
18
18
  };
19
19
  use http_body_util::{combinators::BoxBody, Full};
20
20
  use itsi_error::Result;
21
- use moka::sync::Cache;
21
+ use parking_lot::{Mutex, RwLock};
22
22
  use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
23
+ use quick_cache::sync::Cache;
23
24
  use serde::Deserialize;
24
25
  use serde_json::json;
25
26
  use sha2::{Digest, Sha256};
@@ -34,7 +35,6 @@ use std::{
34
35
  sync::{Arc, LazyLock},
35
36
  time::{Duration, Instant, SystemTime},
36
37
  };
37
- use tokio::sync::Mutex;
38
38
  use tokio::{fs::File, io::AsyncReadExt};
39
39
 
40
40
  use super::mime_types::get_mime_type;
@@ -51,10 +51,7 @@ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|
51
51
  not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
52
52
  serve_hidden_files: false,
53
53
  allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
54
- miss_cache: Cache::builder()
55
- .max_capacity(1000)
56
- .time_to_live(Duration::from_secs(1))
57
- .build(),
54
+ miss_cache: Arc::new(Cache::new(1000)),
58
55
  })
59
56
  .unwrap()
60
57
  });
@@ -89,14 +86,14 @@ pub struct StaticFileServerConfig {
89
86
  pub headers: Option<HashMap<String, String>>,
90
87
  pub serve_hidden_files: bool,
91
88
  pub allowed_extensions: Vec<String>,
92
- pub miss_cache: Cache<String, NotFoundBehavior>,
89
+ pub miss_cache: Arc<Cache<String, NotFoundBehavior>>,
93
90
  }
94
91
 
95
92
  #[derive(Debug, Clone)]
96
93
  pub struct StaticFileServer {
97
94
  config: Arc<StaticFileServerConfig>,
98
95
  key_to_path: Arc<Mutex<HashMap<String, PathBuf>>>,
99
- cache: Cache<PathBuf, CacheEntry>,
96
+ cache: Arc<Cache<PathBuf, Arc<CacheEntry>>>,
100
97
  }
101
98
 
102
99
  impl Deref for StaticFileServer {
@@ -110,36 +107,50 @@ impl Deref for StaticFileServer {
110
107
  #[derive(Clone, Debug)]
111
108
  struct CacheEntry {
112
109
  content: Arc<Bytes>,
113
- br_encoded: Option<Arc<Bytes>>,
114
- zstd_encoded: Option<Arc<Bytes>>,
115
- gzip_encoded: Option<Arc<Bytes>>,
116
- deflate_encoded: Option<Arc<Bytes>>,
117
- etag: String,
110
+ br: Option<Arc<Bytes>>,
111
+ gz: Option<Arc<Bytes>>,
112
+ zstd: Option<Arc<Bytes>>,
113
+ deflate: Option<Arc<Bytes>>,
118
114
  last_modified: SystemTime,
119
- last_checked: Instant,
115
+ headers_ct: HeaderValue,
116
+ headers_etag: HeaderValue,
117
+ headers_cl: HeaderValue,
118
+ last_modified_http_date: HeaderValue,
119
+ last_checked: Arc<RwLock<Instant>>,
120
120
  }
121
121
 
122
+ static HEADER_VALUE_ZSTD: HeaderValue = HeaderValue::from_static("zstd");
123
+ static HEADER_VALUE_GZIP: HeaderValue = HeaderValue::from_static("gzip");
124
+ static HEADER_VALUE_BR: HeaderValue = HeaderValue::from_static("br");
125
+ static HEADER_VALUE_DEFLATE: HeaderValue = HeaderValue::from_static("deflate");
126
+
122
127
  impl CacheEntry {
123
128
  pub fn suggest_content_for(
124
129
  &self,
125
130
  supported_encodings: &[HeaderValue],
126
- ) -> (Arc<Bytes>, Option<&str>) {
131
+ ) -> (Arc<Bytes>, Option<HeaderValue>) {
127
132
  for encoding_header in supported_encodings {
128
133
  if let Ok(header_value) = encoding_header.to_str() {
129
134
  for header_value in header_value.split(",").map(|hv| hv.trim()) {
130
135
  for algo in header_value.split(";").map(|hv| hv.trim()) {
131
136
  match algo {
132
- "zstd" if self.zstd_encoded.is_some() => {
133
- return (self.zstd_encoded.clone().unwrap(), Some("zstd"))
137
+ "zstd" if self.zstd.is_some() => {
138
+ return (
139
+ self.zstd.clone().unwrap(),
140
+ Some(HEADER_VALUE_ZSTD.clone()),
141
+ )
134
142
  }
135
- "gzip" if self.gzip_encoded.is_some() => {
136
- return (self.gzip_encoded.clone().unwrap(), Some("gzip"))
143
+ "gzip" if self.gz.is_some() => {
144
+ return (self.gz.clone().unwrap(), Some(HEADER_VALUE_GZIP.clone()))
137
145
  }
138
- "br" if self.br_encoded.is_some() => {
139
- return (self.br_encoded.clone().unwrap(), Some("br"))
146
+ "br" if self.br.is_some() => {
147
+ return (self.br.clone().unwrap(), Some(HEADER_VALUE_BR.clone()))
140
148
  }
141
- "deflate" if self.deflate_encoded.is_some() => {
142
- return (self.deflate_encoded.clone().unwrap(), Some("deflate"))
149
+ "deflate" if self.deflate.is_some() => {
150
+ return (
151
+ self.deflate.clone().unwrap(),
152
+ Some(HEADER_VALUE_DEFLATE.clone()),
153
+ )
143
154
  }
144
155
  _ => {}
145
156
  }
@@ -158,7 +169,7 @@ pub enum ServeRange {
158
169
  }
159
170
 
160
171
  impl CacheEntry {
161
- async fn new(path: PathBuf) -> Result<Self> {
172
+ async fn new(path: PathBuf) -> Result<Arc<Self>> {
162
173
  let (bytes, last_modified) = read_entire_file(&path).await?;
163
174
  let etag = {
164
175
  let mut hasher = Sha256::new();
@@ -166,23 +177,29 @@ impl CacheEntry {
166
177
  let result = hasher.finalize();
167
178
  general_purpose::STANDARD.encode(result)
168
179
  };
169
- Ok(CacheEntry {
180
+ let headers_ct = get_mime_type(&path);
181
+ let headers_etag = format!(r#"W/"{etag}""#).parse().unwrap();
182
+ let headers_cl = ((bytes.len() as u64).to_string()).parse().unwrap();
183
+ Ok(Arc::new(CacheEntry {
170
184
  content: Arc::new(bytes),
171
- gzip_encoded: read_variant(&path, "gz").await.map(Arc::new),
172
- br_encoded: read_variant(&path, "br").await.map(Arc::new),
173
- zstd_encoded: read_variant(&path, "zstd").await.map(Arc::new),
174
- deflate_encoded: read_variant(&path, "deflate").await.map(Arc::new),
185
+ gz: read_variant(&path, "gz").await.map(Arc::new),
186
+ br: read_variant(&path, "br").await.map(Arc::new),
187
+ zstd: read_variant(&path, "zstd").await.map(Arc::new),
188
+ deflate: read_variant(&path, "deflate").await.map(Arc::new),
189
+ headers_ct,
190
+ headers_etag,
191
+ headers_cl,
175
192
  last_modified,
176
- etag,
177
- last_checked: Instant::now(),
178
- })
193
+ last_modified_http_date: format_http_date_header(last_modified),
194
+ last_checked: Arc::new(RwLock::new(Instant::now())),
195
+ }))
179
196
  }
180
197
 
181
198
  async fn new_virtual_listing(
182
199
  path: PathBuf,
183
200
  config: &StaticFileServerConfig,
184
201
  accept: ResponseFormat,
185
- ) -> Self {
202
+ ) -> Arc<Self> {
186
203
  let directory_listing: Bytes =
187
204
  generate_directory_listing(path.parent().unwrap(), config, accept)
188
205
  .await
@@ -194,16 +211,23 @@ impl CacheEntry {
194
211
  let result = hasher.finalize();
195
212
  general_purpose::STANDARD.encode(result)
196
213
  };
197
- CacheEntry {
214
+ let headers_ct = get_mime_type(&path);
215
+ let headers_etag = format!(r#"W/"{etag}""#).parse().unwrap();
216
+ let headers_cl = directory_listing.len().to_string().parse().unwrap();
217
+ let last_modified = SystemTime::now();
218
+ Arc::new(CacheEntry {
198
219
  content: Arc::new(directory_listing),
199
- gzip_encoded: None,
200
- br_encoded: None,
201
- zstd_encoded: None,
202
- deflate_encoded: None,
203
- last_modified: SystemTime::now(),
204
- etag,
205
- last_checked: Instant::now(),
206
- }
220
+ gz: None,
221
+ br: None,
222
+ zstd: None,
223
+ deflate: None,
224
+ headers_ct,
225
+ headers_etag,
226
+ headers_cl,
227
+ last_modified,
228
+ last_modified_http_date: format_http_date_header(last_modified),
229
+ last_checked: Arc::new(RwLock::new(Instant::now())),
230
+ })
207
231
  }
208
232
  }
209
233
 
@@ -221,7 +245,7 @@ struct ServeCacheArgs<'a>(
221
245
 
222
246
  impl StaticFileServer {
223
247
  pub fn new(config: StaticFileServerConfig) -> Result<Self> {
224
- let cache = Cache::builder().max_capacity(config.max_entries).build();
248
+ let cache = Arc::new(Cache::new(config.max_entries as usize));
225
249
  if !config.root_dir.exists() {
226
250
  return Err(ItsiError::InternalError(format!(
227
251
  "Root directory {} for static file server doesn't exist",
@@ -417,11 +441,16 @@ impl StaticFileServer {
417
441
  if let Some(cached_nf) = self.miss_cache.get(key) {
418
442
  return Err(cached_nf.clone());
419
443
  }
420
- // First check if we have a cached mapping for this key
421
- if let Some(path) = self.key_to_path.lock().await.get(key) {
444
+
445
+ let path = {
446
+ let guard = self.key_to_path.lock();
447
+ guard.get(key).cloned()
448
+ };
449
+
450
+ if let Some(path) = path {
422
451
  // Check if the cached entry is still valid
423
- if let Some(entry) = self.cache.get(path) {
424
- let last_check_elapsed = entry.last_checked.elapsed();
452
+ if let Some(entry) = self.cache.get(&path) {
453
+ let last_check_elapsed = entry.last_checked.read().elapsed();
425
454
  if last_check_elapsed < self.config.recheck_interval {
426
455
  // Entry is still fresh, use it
427
456
  return Ok(ResolvedAsset {
@@ -433,15 +462,13 @@ impl StaticFileServer {
433
462
  }
434
463
 
435
464
  // Entry is stale, check if file has changed
436
- if let Ok(metadata) = tokio::fs::metadata(path).await {
465
+ if let Ok(metadata) = tokio::fs::metadata(&path).await {
437
466
  if metadata
438
467
  .modified()
439
468
  .is_ok_and(|modified| modified == entry.last_modified)
440
469
  {
441
470
  // File hasn't changed, just update last_checked
442
- let mut entry = entry;
443
- entry.last_checked = Instant::now();
444
- self.cache.insert(path.clone(), entry.clone());
471
+ *entry.last_checked.write() = Instant::now();
445
472
  return Ok(ResolvedAsset {
446
473
  path: path.clone(),
447
474
  cache_entry: Some(entry.clone()),
@@ -453,17 +480,19 @@ impl StaticFileServer {
453
480
  // File has changed, check if it's still cacheable
454
481
  if metadata.len() > self.config.max_file_size {
455
482
  // File is now too large, remove from cache
456
- self.cache.invalidate(path);
457
- self.key_to_path.lock().await.remove(key);
483
+ self.cache.remove(&path);
484
+ self.key_to_path.lock().remove(key);
458
485
  }
459
486
  }
460
487
  }
461
488
  }
462
489
 
463
- // No valid cached entry, resolve the key to a file path
464
- let decoded_key = percent_decode_str(key).decode_utf8_lossy();
465
- let normalized_path = normalize_path(decoded_key)
466
- .ok_or(NotFoundBehavior::Error(NOT_FOUND_RESPONSE.clone()))?;
490
+ let normalized_path = normalize_path(if key.contains('%') {
491
+ percent_decode_str(key).decode_utf8_lossy()
492
+ } else {
493
+ Cow::Borrowed(key)
494
+ })
495
+ .ok_or(NotFoundBehavior::Error(NOT_FOUND_RESPONSE.clone()))?;
467
496
 
468
497
  if !self.config.serve_hidden_files
469
498
  && normalized_path
@@ -484,7 +513,6 @@ impl StaticFileServer {
484
513
  let cache_entry = if metadata.len() <= self.config.max_file_size {
485
514
  self.key_to_path
486
515
  .lock()
487
- .await
488
516
  .insert(key.to_string(), full_path.clone());
489
517
  let cache_entry = CacheEntry::new(full_path.clone()).await.unwrap();
490
518
  self.cache.insert(full_path.clone(), cache_entry.clone());
@@ -547,7 +575,6 @@ impl StaticFileServer {
547
575
  let index_path = index_file.unwrap();
548
576
  self.key_to_path
549
577
  .lock()
550
- .await
551
578
  .insert(key.to_string(), index_path.clone());
552
579
  let cache_entry = CacheEntry::new(index_path.clone()).await.unwrap();
553
580
  self.cache.insert(index_path.clone(), cache_entry.clone());
@@ -574,7 +601,6 @@ impl StaticFileServer {
574
601
  .await;
575
602
  self.key_to_path
576
603
  .lock()
577
- .await
578
604
  .insert(key.to_string(), virtual_path.clone());
579
605
  self.cache.insert(virtual_path.clone(), cache_entry.clone());
580
606
  return Ok(ResolvedAsset {
@@ -596,7 +622,6 @@ impl StaticFileServer {
596
622
  if html_meta.is_file() {
597
623
  self.key_to_path
598
624
  .lock()
599
- .await
600
625
  .insert(key.to_string(), html_path.clone());
601
626
  let cache_entry = if html_meta.len() <= self.config.max_file_size {
602
627
  let cache_entry = CacheEntry::new(html_path.clone()).await.unwrap();
@@ -764,7 +789,7 @@ impl StaticFileServer {
764
789
  content_length.to_string()
765
790
  },
766
791
  )
767
- .header("Last-Modified", format_http_date(last_modified));
792
+ .header("Last-Modified", format_http_date_header(last_modified));
768
793
 
769
794
  if let Some(range) = content_range {
770
795
  builder = builder.header("Content-Range", range);
@@ -783,8 +808,8 @@ impl StaticFileServer {
783
808
  None,
784
809
  None,
785
810
  get_mime_type(&file),
786
- (end_idx - start) as usize,
787
- last_modified,
811
+ ((end_idx - start) as usize).to_string().parse().unwrap(),
812
+ format_http_date_header(last_modified),
788
813
  content_range,
789
814
  &self.headers,
790
815
  self.stream_file_range(file, start, end_idx).await.unwrap(),
@@ -795,8 +820,8 @@ impl StaticFileServer {
795
820
  None,
796
821
  None,
797
822
  get_mime_type(&file),
798
- content_length as usize,
799
- last_modified,
823
+ (content_length as usize).to_string().parse().unwrap(),
824
+ format_http_date_header(last_modified),
800
825
  content_range,
801
826
  &self.headers,
802
827
  self.stream_file(file).await.unwrap(),
@@ -870,7 +895,10 @@ impl StaticFileServer {
870
895
  content_length.to_string()
871
896
  },
872
897
  )
873
- .header("Last-Modified", format_http_date(cache_entry.last_modified));
898
+ .header(
899
+ "Last-Modified",
900
+ format_http_date_header(cache_entry.last_modified),
901
+ );
874
902
 
875
903
  if let Some(range) = content_range {
876
904
  builder = builder.header("Content-Range", range);
@@ -883,19 +911,13 @@ impl StaticFileServer {
883
911
  let start_idx = start as usize;
884
912
  let end_idx = std::cmp::min((adjusted_end + 1) as usize, cache_entry.content.len());
885
913
  let range_bytes = cache_entry.content.slice(start_idx..end_idx);
886
- let etag = {
887
- let mut hasher = Sha256::new();
888
- hasher.update(&range_bytes);
889
- let result = hasher.finalize();
890
- general_purpose::STANDARD.encode(result)
891
- };
892
914
  build_file_response(
893
915
  status,
894
916
  None,
895
- Some(&etag),
896
- get_mime_type(path),
897
- range_bytes.len(),
898
- cache_entry.last_modified,
917
+ Some(cache_entry.headers_etag.clone()),
918
+ cache_entry.headers_ct.clone(),
919
+ range_bytes.len().to_string().parse().unwrap(),
920
+ cache_entry.last_modified_http_date.clone(),
899
921
  content_range,
900
922
  &self.headers,
901
923
  BoxBody::new(Full::new(range_bytes)),
@@ -907,10 +929,10 @@ impl StaticFileServer {
907
929
  build_file_response(
908
930
  status,
909
931
  encoding,
910
- Some(&cache_entry.etag),
911
- get_mime_type(path),
912
- content_length as usize,
913
- cache_entry.last_modified,
932
+ Some(cache_entry.headers_etag.clone()),
933
+ cache_entry.headers_ct.clone(),
934
+ cache_entry.headers_cl.clone(),
935
+ cache_entry.last_modified_http_date.clone(),
914
936
  content_range,
915
937
  &self.headers,
916
938
  body,
@@ -920,16 +942,11 @@ impl StaticFileServer {
920
942
 
921
943
  pub async fn invalidate_cache(&self, path: &Path) {
922
944
  if let Ok(path_buf) = path.to_path_buf().canonicalize() {
923
- self.cache.invalidate(&path_buf);
945
+ self.cache.remove(&path_buf);
924
946
  }
925
947
  }
926
948
  }
927
949
 
928
- fn format_http_date(last_modified: SystemTime) -> String {
929
- let datetime = DateTime::<Utc>::from(last_modified);
930
- datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
931
- }
932
-
933
950
  async fn read_entire_file(path: &Path) -> std::io::Result<(Bytes, SystemTime)> {
934
951
  let metadata = tokio::fs::metadata(path).await?;
935
952
  let last_modified = metadata.modified()?;
@@ -962,6 +979,14 @@ async fn read_variant(path: &Path, ext: &str) -> Option<Bytes> {
962
979
  None
963
980
  }
964
981
 
982
+ fn format_http_date_header(time: SystemTime) -> HeaderValue {
983
+ DateTime::<Utc>::from(time)
984
+ .format("%a, %d %b %Y %H:%M:%S GMT")
985
+ .to_string()
986
+ .parse()
987
+ .unwrap()
988
+ }
989
+
965
990
  fn build_ok_body(bytes: Arc<Bytes>) -> BoxBody<Bytes, Infallible> {
966
991
  BoxBody::new(Full::new(bytes.as_ref().clone()))
967
992
  }
@@ -977,39 +1002,46 @@ fn build_not_modified_response() -> http::Response<BoxBody<Bytes, Infallible>> {
977
1002
  #[allow(clippy::too_many_arguments)]
978
1003
  fn build_file_response(
979
1004
  status: StatusCode,
980
- content_encoding: Option<&str>,
981
- etag: Option<&str>,
982
- content_type: &str,
983
- content_length: usize,
984
- last_modified: SystemTime,
1005
+ content_encoding: Option<HeaderValue>,
1006
+ etag: Option<HeaderValue>,
1007
+ content_type: HeaderValue,
1008
+ content_length: HeaderValue,
1009
+ last_modified_http_date: HeaderValue,
985
1010
  range_header: Option<String>,
986
1011
  headers: &Option<HashMap<String, String>>,
987
1012
  body: BoxBody<Bytes, Infallible>,
988
1013
  ) -> http::Response<BoxBody<Bytes, Infallible>> {
989
- let mut builder = Response::builder()
990
- .status(status)
991
- .header(CONTENT_TYPE, content_type)
992
- .header(CONTENT_LENGTH, content_length)
993
- .header(LAST_MODIFIED, format_http_date(last_modified));
1014
+ let mut response = Response::new(body);
994
1015
 
995
- if let Some(etag) = etag {
996
- builder = builder.header(ETAG, etag);
997
- }
1016
+ *response.status_mut() = status;
1017
+ let headers_mut = response.headers_mut();
1018
+
1019
+ headers_mut.insert(CONTENT_TYPE, content_type);
1020
+ headers_mut.insert(CONTENT_LENGTH, content_length);
1021
+ headers_mut.insert(LAST_MODIFIED, last_modified_http_date);
998
1022
 
999
1023
  if let Some(content_encoding) = content_encoding {
1000
- builder = builder.header(CONTENT_ENCODING, content_encoding);
1024
+ headers_mut.insert(CONTENT_ENCODING, content_encoding);
1025
+ }
1026
+
1027
+ if let Some(etag) = etag {
1028
+ headers_mut.insert(ETAG, etag);
1001
1029
  }
1002
1030
 
1003
- if let Some(range) = range_header {
1004
- builder = builder.header(CONTENT_RANGE, range);
1031
+ if let Some(range) = range_header.and_then(|r| r.parse().ok()) {
1032
+ headers_mut.insert(CONTENT_RANGE, range);
1005
1033
  }
1034
+
1006
1035
  if let Some(headers) = headers {
1007
1036
  for (key, value) in headers {
1008
- builder = builder.header(key, value);
1037
+ if let (Ok(parsed_key), Ok(parsed_value)) =
1038
+ (key.parse::<HeaderName>(), value.parse::<HeaderValue>())
1039
+ {
1040
+ headers_mut.insert(parsed_key, parsed_value);
1041
+ }
1009
1042
  }
1010
1043
  }
1011
-
1012
- builder.body(body).unwrap()
1044
+ response
1013
1045
  }
1014
1046
 
1015
1047
  // Helper function to check if a file is too old based on If-Modified-Since
@@ -1049,7 +1081,7 @@ fn normalize_path(path: Cow<'_, str>) -> Option<PathBuf> {
1049
1081
  #[derive(Debug)]
1050
1082
  struct ResolvedAsset {
1051
1083
  path: PathBuf,
1052
- cache_entry: Option<CacheEntry>,
1084
+ cache_entry: Option<Arc<CacheEntry>>,
1053
1085
  metadata: Option<Metadata>,
1054
1086
  redirect_to: Option<String>,
1055
1087
  }
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "itsi_tracing"
3
3
  version = "0.1.0"
4
- edition = "2024"
4
+ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  tracing = { version = "0.1.41", features = ["attributes"] }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.14"
5
+ VERSION = "0.2.15"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.14
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -10,61 +10,61 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rack
13
+ name: json
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.6'
18
+ version: '2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ">="
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '1.6'
25
+ version: '2'
26
26
  - !ruby/object:Gem::Dependency
27
- name: json
27
+ name: prism
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '2'
32
+ version: '1.4'
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: '2'
39
+ version: '1.4'
40
40
  - !ruby/object:Gem::Dependency
41
- name: rb_sys
41
+ name: rack
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.9.91
46
+ version: '1.6'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.9.91
53
+ version: '1.6'
54
54
  - !ruby/object:Gem::Dependency
55
- name: prism
55
+ name: rb_sys
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.4'
60
+ version: 0.9.91
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.4'
67
+ version: 0.9.91
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: ruby-lsp
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -536,7 +536,7 @@ files:
536
536
  - lib/shell_completions/completions.rb
537
537
  homepage: https://itsi.fyi
538
538
  licenses:
539
- - MIT
539
+ - LGPL-3.0
540
540
  metadata:
541
541
  homepage_uri: https://itsi.fyi
542
542
  source_code_uri: https://github.com/wouterken/itsi