schemaless_rest_api 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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