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 +4 -4
- data/lib/schemaless_rest_api/entities.rb +51 -0
- data/lib/schemaless_rest_api/mongo_client.rb +50 -10
- data/lib/schemaless_rest_api/rest_server.rb +21 -28
- data/lib/schemaless_rest_api/seed.rb +27 -0
- data/lib/schemaless_rest_api/server_utils.rb +11 -1
- data/lib/schemaless_rest_api/swagger/swagger.html +1 -2
- data/lib/schemaless_rest_api/swagger_builder.rb +3 -3
- data/lib/schemaless_rest_api/version.rb +1 -1
- data/lib/schemaless_rest_api.rb +6 -16
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f0bc22701cb597ffc52c1e9544b6537546001748ce65e92456e5af32e99fbdd
|
4
|
+
data.tar.gz: c40d93389fbfe7f98fa7d05f262b2da7b806dced3223f13643122f0359c522b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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,
|
41
|
+
def find_all(model, params, paginate: true)
|
28
42
|
collection = MongoClient.client[model]
|
29
|
-
|
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
|
64
|
-
summary[:db] = MongoClient.client.
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
112
|
+
results.first.to_json
|
122
113
|
else
|
123
|
-
return not_have(id) unless
|
114
|
+
return not_have(id) unless id?(model, id)
|
124
115
|
|
125
|
-
|
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
|
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
|
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
|
-
|
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
|
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),
|
data/lib/schemaless_rest_api.rb
CHANGED
@@ -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.
|
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-
|
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
|