schemaless_rest_api 0.5.1 → 0.7.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: 49fba7a856b268a6077d6597c20687d0d056cce2049fb5c0590da046acebf91f
4
- data.tar.gz: 2ac45839af0b7d4e273812bcf6a4a9d3f750d958285c3f9fcfccf143ee4d8c57
3
+ metadata.gz: 9f0bc22701cb597ffc52c1e9544b6537546001748ce65e92456e5af32e99fbdd
4
+ data.tar.gz: c40d93389fbfe7f98fa7d05f262b2da7b806dced3223f13643122f0359c522b0
5
5
  SHA512:
6
- metadata.gz: 5c545f176d807dff6894b10a54aba45737968157247261b1a973ae471ec60155834a8847a43edb3c2ee50ad2a2b562846f5513479112402f3013b7da6a84a0c2
7
- data.tar.gz: 35f4ed2bc211ac4bf163296b69c29362a7d13a3c705898e7a5a07438d3c8b071907acfd0792fb9b84e81519e74b6cfd95276156948ae422d27886a13ebb7421c
6
+ metadata.gz: ed664da78bf2832449243ef73597521432e6f71dddbddb34a0c3cbcfd3ab9de105b215ad83e7775380a9cf103d966981f2c4036f3215c1eb500a668ee8e0f736
7
+ data.tar.gz: 27438bf5b3196e88c7dc79171bc4baefd537fb0b049fa796207c7fb2ba131781680ca931b79f9660c5a4d325bfd0cc5f9cb26d41a4090b251b374e4675105dc2
@@ -1,9 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # typed: true
4
+
3
5
  # Entities mapped by environment variables
4
6
  class Entities
5
7
  @models = {}
6
8
  class << self
9
+ # @return [Hash] Hash of models
7
10
  attr_accessor :models
11
+
12
+ def page_data(model, params, values)
13
+ page = params[:page].to_i.positive? ? params[:page].to_i : 1
14
+ page_size = params[:page_size].to_i.positive? ? params[:page_size].to_i : 10
15
+ total_count = values.count
16
+ total_pages = (total_count / page_size.to_f).ceil
17
+ page_res = {
18
+ current_page: page,
19
+ page_size: page_size,
20
+ total_count: total_count,
21
+ total_pages: total_pages,
22
+ _links: {
23
+ self: { href: "/#{model}?page=#{page}&page_size=#{page_size}" }
24
+ }
25
+ }
26
+ page_res[:_links][:next] = { href: "/#{model}?page=#{page + 1}&page_size=#{page_size}" } if page < total_pages
27
+ page_res[:_links][:prev] = { href: "/#{model}?page=#{page - 1}&page_size=#{page_size}" } if page > 1
28
+ page_res
29
+ end
30
+
31
+ def query_from_params(params)
32
+ query = params.dup
33
+ query.delete(:page)
34
+ query.delete(:page_size)
35
+ query
36
+ end
37
+
38
+ # Find all values for given model querying via params
39
+ def find_all(model, params)
40
+ query = query_from_params params
41
+ total_items = if query == {}
42
+ Entities.models[model].values
43
+ else
44
+ Entities.models[model].values.find_all do |val|
45
+ val[query.keys[0].to_s].to_s == query.values[0]
46
+ end
47
+ end
48
+ pagination = page_data(model, params, total_items)
49
+ skip = (pagination[:current_page] - 1) * pagination[:page_size]
50
+
51
+ items = total_items.drop(skip).take(pagination[:page_size])
52
+ response = {
53
+ _embedded: {},
54
+ pagination: pagination
55
+ }
56
+ response[:_embedded][model.to_sym] = items
57
+ response
58
+ end
8
59
  end
9
60
  end
@@ -1,7 +1,10 @@
1
- require "mongo"
1
+ # frozen_string_literal: true
2
2
 
3
3
  # typed: false - MongoClient new
4
4
 
5
+ require "mongo"
6
+
7
+ # Client to talk to Mongo database
5
8
  module MongoClient
6
9
  class << self
7
10
  # @return Client to work with MongoDb
@@ -13,25 +16,62 @@ module MongoClient
13
16
  end
14
17
 
15
18
  def find(model, id)
16
- find_all(model, { id: id })
19
+ find_all(model, { id: id }, false)[:items]
17
20
  end
18
21
 
19
- def get_all(model)
20
- collection = MongoClient.client[model]
21
- collection.find.collect do |match|
22
- match.delete("_id")
23
- match
24
- end
22
+ def page_data(model, params, collection)
23
+ page = params[:page].to_i.positive? ? params[:page].to_i : 1
24
+ page_size = params[:page_size].to_i.positive? ? params[:page_size].to_i : 10
25
+ total_count = collection.count_documents({})
26
+ total_pages = (total_count / page_size.to_f).ceil
27
+ page_res = {
28
+ current_page: page,
29
+ page_size: page_size,
30
+ total_count: total_count,
31
+ total_pages: total_pages,
32
+ _links: {
33
+ self: { href: "/#{model}?page=#{page}&page_size=#{page_size}" }
34
+ }
35
+ }
36
+ page_res[:_links][:next] = { href: "/#{model}?page=#{page + 1}&page_size=#{page_size}" } if page < total_pages
37
+ page_res[:_links][:prev] = { href: "/#{model}?page=#{page - 1}&page_size=#{page_size}" } if page > 1
38
+ page_res
25
39
  end
26
40
 
27
- def find_all(model, query)
41
+ def find_all(model, params, paginate: true)
28
42
  collection = MongoClient.client[model]
29
- collection.find(query).collect do |match|
43
+ pagination = page_data(model, params, collection)
44
+ skip = (pagination[:current_page] - 1) * pagination[:page_size]
45
+ query = query_from_params params
46
+
47
+ items = get_items collection, query, skip, pagination[:page_size]
48
+ results = { _embedded: {} }
49
+ results[:_embedded][model.to_sym] = items
50
+ results[:pagination] = pagination if paginate
51
+ results
52
+ end
53
+
54
+ def get_items(collection, query, skip, page_size)
55
+ if query == {}
56
+ return collection.find.skip(skip).limit(page_size).collect do |match|
57
+ match.delete("_id")
58
+ match
59
+ end
60
+ end
61
+
62
+ collection.find(query).skip(skip).limit(page_size).collect do |match|
30
63
  match.delete("_id")
31
64
  match
32
65
  end
33
66
  end
34
67
 
68
+ def query_from_params(params)
69
+ query = params.dup
70
+ query.delete(:page)
71
+ query.delete(:page_size)
72
+ query
73
+ end
74
+
35
75
  def update(model, id, data)
36
76
  collection = MongoClient.client[model]
37
77
  collection.update_one({ id: id }, { id: id, **data })
@@ -3,6 +3,8 @@
3
3
  require "sinatra"
4
4
  require "puma"
5
5
  require "route_downcaser"
6
+ require "prometheus/middleware/collector"
7
+ require "prometheus/middleware/exporter"
6
8
  require_relative "server_utils"
7
9
 
8
10
  # Server with endpoints generated based on Entities with CRUD operations for them
@@ -11,6 +13,8 @@ class RestServer < Sinatra::Base
11
13
  enable :logging if ENV["debug"] == "true"
12
14
  set :bind, "0.0.0.0"
13
15
  use RouteDowncaser::DowncaseRouteMiddleware
16
+ use Prometheus::Middleware::Collector
17
+ use Prometheus::Middleware::Exporter
14
18
  helpers ServerUtils
15
19
 
16
20
  before do
@@ -29,7 +33,7 @@ class RestServer < Sinatra::Base
29
33
  end
30
34
 
31
35
  SWAGGER_FILES = %w[index.css swagger.html swagger-initializer.js swagger-ui-bundle.js swagger-ui-standalone-preset.js
32
- swagger-ui.css]
36
+ swagger-ui.css].freeze
33
37
 
34
38
  SWAGGER_FILES.each do |filename|
35
39
  get "/#{filename.gsub(".html", "")}" do
@@ -50,25 +54,18 @@ class RestServer < Sinatra::Base
50
54
  SwaggerBuilder.build_swagger_for(Entities.models, request.host_with_port)
51
55
  end
52
56
 
53
- def has_id?(model, id)
54
- Entities.models[model].key?(id)
55
- end
56
-
57
- def not_have(id)
58
- [404, JSON.generate({ error: "'#{id}' not found" })]
59
- end
60
-
61
57
  get "/" do
62
58
  summary = { models: Entities.models.keys.to_s,
63
- docs_url: "<a href=/swagger>Swagger docs</a>" }
64
- summary[:db] = MongoClient.client.summary.to_s if ENV["mongodb"]
65
- JSON.generate(summary)
59
+ docs_url: "<a href='#{ENV["base_path"]}/swagger'>Swagger docs</a>" }
60
+ summary[:db] = MongoClient.client.options if ENV["mongodb"]
61
+ summary.to_json
66
62
  rescue StandardError => e
67
63
  [500, e.message]
68
64
  end
69
65
 
70
66
  Entities.models.each_key do |model|
71
67
  post "/#{model.downcase}" do
68
+ content_type :json
72
69
  request_body = request.body.read
73
70
  data = {}
74
71
  data = JSON.parse(request_body) unless request_body.empty?
@@ -95,34 +92,28 @@ class RestServer < Sinatra::Base
95
92
  end
96
93
 
97
94
  get "/#{model.downcase}" do
95
+ content_type :json
98
96
  if ENV["mongodb"]
99
- if params == {}
100
- JSON.generate(MongoClient.get_all(model))
101
- else
102
- [200, JSON.generate(MongoClient.find_all(model, params))]
103
- end
97
+ MongoClient.find_all(model, params).to_json
104
98
  else
105
- return JSON.generate(Entities.models[model].values) if params == {}
106
-
107
- matching_values = Entities.models[model].values.find_all do |val|
108
- val[params.keys[0]].to_s == params.values[0]
109
- end
110
- return JSON.generate(matching_values)
99
+ matching_values = Entities.find_all(model, params)
100
+ return matching_values.to_json
111
101
  end
112
102
  rescue StandardError => e
113
103
  [404, "Nothing found using #{params}. Only first param considered. #{e.message}"]
114
104
  end
115
105
 
116
106
  get "/#{model.downcase}/:id" do |id|
107
+ content_type :json
117
108
  if ENV["mongodb"]
118
109
  results = MongoClient.find(model, id)
119
110
  return not_have(id) unless results.first
120
111
 
121
- JSON.generate(results.first)
112
+ results.first.to_json
122
113
  else
123
- return not_have(id) unless has_id?(model, id)
114
+ return not_have(id) unless id?(model, id)
124
115
 
125
- JSON.generate(Entities.models[model][id])
116
+ Entities.models[model][id].to_json
126
117
  end
127
118
  end
128
119
 
@@ -134,6 +125,7 @@ class RestServer < Sinatra::Base
134
125
  end
135
126
 
136
127
  put "/#{model.downcase}/:id" do |id|
128
+ content_type :json
137
129
  data = JSON.parse(request.body.read)
138
130
  if ENV["mongodb"]
139
131
  results = MongoClient.find(model, id)
@@ -141,7 +133,7 @@ class RestServer < Sinatra::Base
141
133
 
142
134
  MongoClient.update(model, id, data)
143
135
  else
144
- return not_have(id) unless has_id?(model, id)
136
+ return not_have(id) unless id?(model, id)
145
137
 
146
138
  Entities.models[model][id] = data
147
139
  end
@@ -149,13 +141,14 @@ class RestServer < Sinatra::Base
149
141
  end
150
142
 
151
143
  delete "/#{model.downcase}/:id" do |id|
144
+ content_type :json
152
145
  if ENV["mongodb"]
153
146
  results = MongoClient.find(model, id)
154
147
  return not_have(id) unless results.first
155
148
 
156
149
  MongoClient.delete(model, id)
157
150
  else
158
- return not_have(id) unless has_id?(model, id)
151
+ return not_have(id) unless id?(model, id)
159
152
 
160
153
  Entities.models[model].delete(id)
161
154
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: true
4
+
5
+ def init_based_on_seed
6
+ return if ENV["mongodb"]
7
+
8
+ puts "Seeding db based on '#{SEED_FILE}'"
9
+ seed_data = JSON.parse(File.read(SEED_FILE))
10
+ seed_data.each do |entity, values|
11
+ Entities.models[entity.to_sym] = {} unless Entities.models[entity.to_sym]
12
+ seed_in_memory_data(entity, values)
13
+ end
14
+ end
15
+
16
+ def seed_in_memory_data(entity, values)
17
+ unless values.is_a? Array
18
+ SchemalessRestApi.logger.warning "Entity '#{entity}' doesn't have array, skipping"
19
+ return
20
+ end
21
+
22
+ values.each do |value|
23
+ next unless value["id"]
24
+
25
+ Entities.models[entity.to_sym][value["id"].to_s] = value
26
+ end
27
+ end
@@ -13,7 +13,15 @@ module ServerUtils
13
13
  end
14
14
  end
15
15
 
16
- private def log_structured(messages)
16
+ def id?(model, id)
17
+ Entities.models[model].key?(id)
18
+ end
19
+
20
+ def not_have(id)
21
+ [404, JSON.generate({ error: "'#{id}' not found" })]
22
+ end
23
+
24
+ def log_structured(messages)
17
25
  log_msg = {
18
26
  method: request.request_method,
19
27
  path: request.fullpath,
@@ -24,4 +32,6 @@ module ServerUtils
24
32
  end
25
33
  SchemalessRestApi.logger.info(log_msg)
26
34
  end
35
+
36
+ private :log_structured
27
37
  end
@@ -6,8 +6,7 @@
6
6
  <title>Swagger UI</title>
7
7
  <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
8
8
  <link rel="stylesheet" type="text/css" href="index.css" />
9
- <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
10
- <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
9
+ <link rel="icon" type="image/png" href="./favicon.ico" sizes="32x32" />
11
10
  </head>
12
11
 
13
12
  <body>
@@ -13,7 +13,7 @@ class SwaggerBuilder
13
13
  doc["info"] = build_info(models)
14
14
  doc["host"] = host_with_port
15
15
  doc["schemes"] = "http"
16
- doc["basePath"] = "/"
16
+ doc["basePath"] = "/#{ENV["base_path"]}"
17
17
  doc["paths"] = generate_paths_for models
18
18
  JSON.generate(doc)
19
19
  end
@@ -57,7 +57,7 @@ class SwaggerBuilder
57
57
  }
58
58
  },
59
59
  get: {
60
- parameters: [
60
+ parameters: [
61
61
  {
62
62
  name: "params",
63
63
  description: "Any key can be used to filter #{model} by",
@@ -94,7 +94,7 @@ class SwaggerBuilder
94
94
  end
95
95
 
96
96
  def paths_at_id(model)
97
- {
97
+ {
98
98
  get: {
99
99
  description: "Data of #{model} at passed id",
100
100
  parameters: id_params(model),
@@ -3,5 +3,5 @@
3
3
  # typed: true
4
4
 
5
5
  module SchemalessRestApi
6
- VERSION = "0.5.1"
6
+ VERSION = "0.7.0"
7
7
  end
@@ -5,6 +5,7 @@
5
5
  require_relative "schemaless_rest_api/version"
6
6
  require_relative "schemaless_rest_api/entities"
7
7
  require_relative "schemaless_rest_api/swagger_builder"
8
+ require_relative "schemaless_rest_api/seed"
8
9
  require "tapioca"
9
10
  require "logger"
10
11
  require "ougai"
@@ -12,6 +13,7 @@ require "json"
12
13
  require "securerandom"
13
14
 
14
15
  ENV["APP_ENV"] ||= "production"
16
+ ENV["base_path"] ||= ""
15
17
 
16
18
  # Global params for Schemalass REST API
17
19
  module SchemalessRestApi
@@ -22,6 +24,9 @@ module SchemalessRestApi
22
24
  @logger = Logger.new($stdout)
23
25
  @log_type = :basic
24
26
  end
27
+ LOGLEVELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
28
+ log_level ||= LOGLEVELS.index ENV.fetch("LOG_LEVEL", "INFO")
29
+ @logger.level = log_level
25
30
 
26
31
  class << self
27
32
  # @return Logger
@@ -52,22 +57,7 @@ end
52
57
 
53
58
  SEED_FILE = "db.json"
54
59
 
55
- if File.exist? SEED_FILE
56
- puts "Seeding db based on '#{SEED_FILE}'"
57
- seed_data = JSON.parse(File.read(SEED_FILE))
58
- seed_data.each do |entity, values|
59
- Entities.models[entity.to_sym] = {} unless Entities.models[entity.to_sym]
60
- if values.is_a? Array
61
- values.each do |value|
62
- next unless value["id"]
63
-
64
- Entities.models[entity.to_sym][value["id"].to_s] = value
65
- end
66
- else
67
- puts "Entity 'entity' doesn't have array, skipping"
68
- end
69
- end
70
- end
60
+ init_based_on_seed if File.exist? SEED_FILE
71
61
 
72
62
  extract_models.each do |model|
73
63
  Entities.models[model.to_sym] = {}
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schemaless_rest_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Garratt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-19 00:00:00.000000000 Z
11
+ date: 2024-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongo
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: prometheus-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: puma
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -124,6 +138,7 @@ files:
124
138
  - lib/schemaless_rest_api/mongo_client.rb
125
139
  - lib/schemaless_rest_api/rest.ico
126
140
  - lib/schemaless_rest_api/rest_server.rb
141
+ - lib/schemaless_rest_api/seed.rb
127
142
  - lib/schemaless_rest_api/server_utils.rb
128
143
  - lib/schemaless_rest_api/swagger/index.css
129
144
  - lib/schemaless_rest_api/swagger/swagger-initializer.js