canvas_erd 0.1.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.
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack/utils"
5
+ require "socket"
6
+
7
+ module CanvasERD
8
+ class WebApplication
9
+ WEB_ROOT = File.expand_path("web", __dir__)
10
+ STATIC_FILES = {
11
+ "/" => ["index.html", "text/html; charset=utf-8"],
12
+ "/assets/app.js" => ["app.js", "application/javascript; charset=utf-8"],
13
+ "/assets/document.js" => ["document.js", "application/javascript; charset=utf-8"],
14
+ "/assets/layout.js" => ["layout.js", "application/javascript; charset=utf-8"],
15
+ "/assets/png.js" => ["png.js", "application/javascript; charset=utf-8"],
16
+ "/assets/styles.css" => ["styles.css", "text/css; charset=utf-8"],
17
+ "/assets/fabric.min.js" => ["vendor/fabric.min.js", "application/javascript; charset=utf-8"],
18
+ "/assets/elk-api.js" => ["vendor/elk-api.js", "application/javascript; charset=utf-8"],
19
+ "/assets/elk-worker.min.js" => ["vendor/elk-worker.min.js", "application/javascript; charset=utf-8"]
20
+ }.freeze
21
+
22
+ attr_reader :schema
23
+
24
+ def initialize(schema:, schema_provider: nil, state: nil, diagram_store: nil)
25
+ @schema = schema
26
+ @schema_provider = schema_provider || -> { schema }
27
+ @diagram_store = diagram_store
28
+ @schema_mutex = Mutex.new
29
+ @document = {
30
+ "format" => "canvas_erd",
31
+ "version" => 1,
32
+ "schema" => schema,
33
+ "state" => state
34
+ }
35
+ end
36
+
37
+ def call(environment)
38
+ method = environment.fetch("REQUEST_METHOD")
39
+ path = environment.fetch("PATH_INFO")
40
+
41
+ status, headers, body = route(method, path, environment)
42
+
43
+ body = [] if method == "HEAD"
44
+ [status, security_headers.merge(headers), body]
45
+ rescue DiagramStore::NotFound => error
46
+ status, headers, body = response(404, "text/plain; charset=utf-8", error.message)
47
+ [status, security_headers.merge(headers), method == "HEAD" ? [] : body]
48
+ rescue DiagramStore::Error => error
49
+ status, headers, body = response(400, "text/plain; charset=utf-8", error.message)
50
+ [status, security_headers.merge(headers), method == "HEAD" ? [] : body]
51
+ end
52
+
53
+ private
54
+
55
+ def route(method, path, environment)
56
+ if path == "/api/diagram"
57
+ return method_not_allowed("GET, HEAD, PUT") unless %w[GET HEAD PUT].include?(method)
58
+
59
+ return diagram_response(environment, write: method == "PUT")
60
+ end
61
+
62
+ return method_not_allowed("GET, HEAD") unless %w[GET HEAD].include?(method)
63
+
64
+ if path == "/api/schema"
65
+ schema_response(environment, refresh: method == "GET")
66
+ elsif path == "/api/document"
67
+ document_response
68
+ elsif path == "/api/diagrams"
69
+ diagrams_response
70
+ elsif STATIC_FILES.key?(path)
71
+ file_response(*STATIC_FILES.fetch(path))
72
+ else
73
+ response(404, "text/plain; charset=utf-8", "Not Found")
74
+ end
75
+ end
76
+
77
+ def method_not_allowed(allow)
78
+ response(405, "text/plain; charset=utf-8", "Method Not Allowed", "allow" => allow)
79
+ end
80
+
81
+ def schema_response(environment, refresh:)
82
+ if refresh && environment.fetch("QUERY_STRING", "").split("&").include?("refresh=1")
83
+ @schema_mutex.synchronize { @schema = @schema_provider.call }
84
+ end
85
+
86
+ response(
87
+ 200,
88
+ "application/json; charset=utf-8",
89
+ JSON.generate(schema),
90
+ "cache-control" => "no-store"
91
+ )
92
+ end
93
+
94
+ def document_response
95
+ document = @document["state"] ? @document : @document.merge("schema" => schema)
96
+ response(
97
+ 200,
98
+ "application/json; charset=utf-8",
99
+ JSON.generate(document),
100
+ "cache-control" => "no-store"
101
+ )
102
+ end
103
+
104
+ def diagrams_response
105
+ return response(404, "text/plain; charset=utf-8", "Diagram storage is not configured") unless @diagram_store
106
+
107
+ response(
108
+ 200,
109
+ "application/json; charset=utf-8",
110
+ JSON.generate("directory" => @diagram_store.relative_directory, "diagrams" => @diagram_store.names),
111
+ "cache-control" => "no-store"
112
+ )
113
+ end
114
+
115
+ def diagram_response(environment, write:)
116
+ return response(404, "text/plain; charset=utf-8", "Diagram storage is not configured") unless @diagram_store
117
+
118
+ name = Rack::Utils.parse_query(environment.fetch("QUERY_STRING", ""))["name"]
119
+ if write
120
+ bytes = environment.fetch("rack.input").read.b
121
+ @diagram_store.write(name, bytes)
122
+ response(200, "application/json; charset=utf-8", JSON.generate("name" => name))
123
+ else
124
+ response(
125
+ 200,
126
+ "image/png",
127
+ @diagram_store.read(name),
128
+ "cache-control" => "no-store"
129
+ )
130
+ end
131
+ end
132
+
133
+ def file_response(relative_path, content_type)
134
+ response(
135
+ 200,
136
+ content_type,
137
+ File.binread(File.join(WEB_ROOT, relative_path)),
138
+ "cache-control" => "no-store"
139
+ )
140
+ end
141
+
142
+ def response(status, content_type, body, headers = {})
143
+ headers = { "content-type" => content_type }.merge(headers)
144
+ [status, headers, [body]]
145
+ end
146
+
147
+ def security_headers
148
+ {
149
+ "content-security-policy" => "default-src 'self'; connect-src 'self'; img-src 'self' data: blob:; " \
150
+ "object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
151
+ "x-content-type-options" => "nosniff",
152
+ "referrer-policy" => "no-referrer"
153
+ }
154
+ end
155
+ end
156
+
157
+ class Server
158
+ class Error < StandardError; end
159
+
160
+ HOST = "127.0.0.1"
161
+
162
+ attr_reader :port
163
+
164
+ def initialize(app:, port: nil, rack_server_class: nil)
165
+ @app = app
166
+ @port = port || available_port
167
+ @rack_server_class = rack_server_class || self.class.rack_server_class
168
+ end
169
+
170
+ def start
171
+ @rack_server_class.start(
172
+ app: @app,
173
+ Host: HOST,
174
+ Port: port,
175
+ environment: "none"
176
+ )
177
+ rescue LoadError => error
178
+ raise Error, "No Rack server handler is available: #{error.message}"
179
+ end
180
+
181
+ def url
182
+ "http://#{HOST}:#{port}/"
183
+ end
184
+
185
+ def self.rack_server_class(loader: method(:load_rack_server_class))
186
+ loader.call("rackup", "Rackup") ||
187
+ loader.call("rack/server", "Rack") ||
188
+ raise(Error, "No Rack server is available in this Rails application")
189
+ end
190
+
191
+ def self.load_rack_server_class(feature, namespace_name)
192
+ require feature
193
+ namespace = Object.const_get(namespace_name)
194
+ namespace.const_get(:Server, false) if namespace.const_defined?(:Server, false)
195
+ rescue LoadError, NameError
196
+ nil
197
+ end
198
+
199
+ private_class_method :load_rack_server_class
200
+
201
+ private
202
+
203
+ def available_port
204
+ socket = TCPServer.new(HOST, 0)
205
+ socket.addr.fetch(1)
206
+ ensure
207
+ socket&.close
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanvasERD
4
+ VERSION = "0.1.0"
5
+ end
6
+