datomic-flare 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flare
4
+ module Errors
5
+ class FlareError < StandardError
6
+ def initialize(message = nil)
7
+ super
8
+ end
9
+ end
10
+
11
+ class RequestError < FlareError
12
+ attr_reader :request, :payload
13
+
14
+ def initialize(message = nil, request: nil, payload: nil)
15
+ @request = request
16
+ @payload = payload
17
+
18
+ super(message)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/typhoeus'
5
+ require 'json'
6
+ require_relative '../components/errors'
7
+
8
+ module Flare
9
+ module HTTP
10
+ DEFAULT_ADDRESS = 'http://localhost:3042'
11
+ ALLOWED_REQUEST_OPTIONS = %i[timeout open_timeout read_timeout write_timeout].freeze
12
+ DEFAULT_FARADAY_ADAPTER = :typhoeus
13
+
14
+ class Client
15
+ def initialize(config)
16
+ @address = if config[:credentials][:address].to_s.strip.empty?
17
+ DEFAULT_ADDRESS
18
+ else
19
+ config[:credentials][:address].to_s.sub(
20
+ %r{/$}, ''
21
+ )
22
+ end
23
+
24
+ @request_options = config.dig(:options, :connection, :request)&.slice(*ALLOWED_REQUEST_OPTIONS) || {}
25
+ @faraday_adapter = config.dig(:options, :connection, :adapter) || DEFAULT_FARADAY_ADAPTER
26
+ end
27
+
28
+ def request(path, payload = nil, debug:, request_method: 'POST')
29
+ url = "#{@address}/#{path}"
30
+ method = request_method.to_s.strip.downcase.to_sym
31
+
32
+ if debug
33
+ debug_payload = { method: method.to_s.upcase, url: }
34
+
35
+ debug_payload[:body] = JSON.parse(payload.to_json) unless payload.nil?
36
+
37
+ return debug_payload
38
+ end
39
+
40
+ response = Faraday.new(request: @request_options) do |faraday|
41
+ faraday.adapter @faraday_adapter
42
+ faraday.response :raise_error
43
+ end.send(method) do |req|
44
+ req.url url
45
+ req.headers['Content-Type'] = 'application/json'
46
+ req.body = payload.to_json unless payload.nil?
47
+ end
48
+
49
+ JSON.parse(response.body)
50
+ rescue Faraday::Error => e
51
+ raise Errors::RequestError.new("#{e.message}: #{e.response[:body]}", request: e, payload:)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flare
4
+ class API
5
+ attr_reader :client
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ def create_database!(payload, debug: nil)
12
+ client.request('datomic/create-database', payload, debug:)
13
+ end
14
+
15
+ def delete_database!(payload, debug: nil)
16
+ client.request(
17
+ 'datomic/delete-database', payload,
18
+ request_method: 'DELETE', debug:
19
+ )
20
+ end
21
+
22
+ def transact!(payload, debug: nil)
23
+ client.request('datomic/transact', payload, debug:)
24
+ end
25
+
26
+ def entity(payload, debug: nil)
27
+ client.request('datomic/entity', payload, request_method: 'GET', debug:)
28
+ end
29
+
30
+ def datoms(payload, debug: nil)
31
+ client.request('datomic/datoms', payload, request_method: 'GET', debug:)
32
+ end
33
+
34
+ def get_database_names(debug: nil)
35
+ client.request(
36
+ 'datomic/get-database-names', request_method: 'GET', debug:
37
+ )
38
+ end
39
+
40
+ def list_databases(debug: nil)
41
+ client.request('datomic/list-databases', request_method: 'GET', debug:)
42
+ end
43
+
44
+ def q(payload, debug: nil)
45
+ client.request('datomic/q', payload, request_method: 'GET', debug:)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../components/http'
4
+ require_relative '../logic/dangerous_override'
5
+
6
+ require_relative 'dsl'
7
+ require_relative 'api'
8
+
9
+ module Flare
10
+ module Controllers
11
+ class Client
12
+ attr_reader :dangerously_override
13
+
14
+ def initialize(config)
15
+ @http_client = HTTP::Client.new(config)
16
+
17
+ @dangerously_override = (config[:dangerously_override] || {}).freeze
18
+ end
19
+
20
+ def meta(debug: nil)
21
+ request('meta', request_method: 'GET', debug:)
22
+ end
23
+
24
+ def api
25
+ @api ||= API.new(self)
26
+ end
27
+
28
+ def dsl
29
+ @dsl ||= DSL.new(self)
30
+ end
31
+
32
+ def request(path, payload = nil, debug:, request_method: 'POST')
33
+ @http_client.request(
34
+ path,
35
+ DangerousOverrideLogic.apply_dangerous_overrides_to_payload(
36
+ path, dangerously_override, payload
37
+ ),
38
+ request_method:, debug:
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'pp'
5
+ require 'tempfile'
6
+
7
+ module Flare
8
+ module Controllers
9
+ module Documentation
10
+ module Formatter
11
+ def self.to_s_and_format(ruby_object)
12
+ code = PP.pp(ruby_object, String.new)
13
+
14
+ format_code(code)
15
+ end
16
+
17
+ def self.format_code(code)
18
+ Tempfile.create(['code', '.rb']) do |file|
19
+ file.write(code)
20
+ file.flush
21
+
22
+ options = { autocorrect: true, formatters: [], cache: false }
23
+
24
+ config_store = RuboCop::ConfigStore.new
25
+ config_store.options_config = './docs/templates/.rubocop.yml'
26
+
27
+ runner = RuboCop::Runner.new(options, config_store)
28
+
29
+ runner.run([file.path])
30
+
31
+ File.read(file.path).strip
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid'
4
+ require 'babosa'
5
+
6
+ require 'json'
7
+
8
+ require_relative 'formatter'
9
+ require_relative '../../ports/dsl/datomic-flare'
10
+ require_relative '../../static/gem'
11
+
12
+ module Flare
13
+ module Controllers
14
+ module Documentation
15
+ module Generator
16
+ FLARE_AS_PEER_ADDRESS = ENV.fetch('FLARE_AS_PEER_ADDRESS', nil)
17
+ FLARE_SOURCE = ENV.fetch('FLARE_SOURCE', nil)
18
+
19
+ def self.handler
20
+ generate_individual_documentations!
21
+ generate_consolidated_readme!
22
+ generate_flare_readme!
23
+ end
24
+
25
+ def self.generate_flare_readme!
26
+ unless File.exist?("#{FLARE_SOURCE}/deps.edn")
27
+ warn "Couldn't find datomic-flare project at '#{FLARE_SOURCE}/deps.edn'"
28
+ return
29
+ end
30
+
31
+ template_path = "#{FLARE_SOURCE}/docs/templates/README.md"
32
+ rendered_path = "#{FLARE_SOURCE}/docs/README.md"
33
+ readme_path = "#{FLARE_SOURCE}/README.md"
34
+
35
+ create_database_and_generate_documentation!(template_path)
36
+
37
+ quick_starts = Dir[
38
+ "#{FLARE_SOURCE}/docs/quick-start/*/README.md"
39
+ ].map do |path|
40
+ markdown = File.read(path)
41
+ source_path = Dir["#{File.dirname(path)}/flare.*"].first
42
+ language = File.extname(source_path).sub('.', '')
43
+ source = File.read(source_path)
44
+
45
+ "#{markdown}\n\n```#{language}\n#{source.strip}\n```"
46
+ end
47
+
48
+ content_with_quick_starts = Liquid::Template.parse(
49
+ File.read(rendered_path)
50
+ ).render(
51
+ stringify_keys(
52
+ {
53
+ quick_starts: quick_starts.join("\n\n")
54
+ }
55
+ )
56
+ )
57
+
58
+ content = Liquid::Template.parse(File.read(rendered_path)).render(
59
+ stringify_keys(
60
+ {
61
+ index: generate_index(content_with_quick_starts),
62
+ quick_starts: quick_starts.join("\n\n")
63
+ }
64
+ )
65
+ )
66
+
67
+ puts "> Generating final datomic-flare/README.md: '#{readme_path}'"
68
+
69
+ File.write(readme_path, content)
70
+ end
71
+
72
+ def self.generate_consolidated_readme!
73
+ content = Liquid::Template.parse(File.read('./docs/README.md')).render(
74
+ stringify_keys(
75
+ {
76
+ gem: GEM,
77
+ dsl: File.read('./docs/dsl.md'),
78
+ api: File.read('./docs/api.md')
79
+ }
80
+ )
81
+ )
82
+
83
+ content = Liquid::Template.parse(File.read('./docs/README.md')).render(
84
+ stringify_keys(
85
+ {
86
+ index: generate_index(content),
87
+ gem: GEM,
88
+ dsl: Liquid::Template.parse(
89
+ File.read('./docs/dsl.md')
90
+ ).render(stringify_keys({ gem: GEM })),
91
+ api: Liquid::Template.parse(
92
+ File.read('./docs/api.md')
93
+ ).render(stringify_keys({ gem: GEM }))
94
+ }
95
+ )
96
+ )
97
+
98
+ puts "> Generating final README: './README.md'"
99
+
100
+ File.write('./README.md', content)
101
+ end
102
+
103
+ def self.generate_index(content)
104
+ sections = []
105
+
106
+ urls = {}
107
+
108
+ content.lines.each do |line|
109
+ next unless line.strip.start_with?('#')
110
+
111
+ base_url = line.strip.split(/#\s+/).last.strip.to_slug.normalize.to_s
112
+ url = base_url
113
+
114
+ url_index = 0
115
+
116
+ while urls.key?(url)
117
+ url_index += 1
118
+ url = "#{base_url}-#{url_index}"
119
+ end
120
+
121
+ urls[url] = true
122
+
123
+ sections << {
124
+ title: line.strip.split(/#\s+/).last.strip,
125
+ url:,
126
+ level: line.strip.split(/\s/).first.strip.size,
127
+ raw: line
128
+ }
129
+ end
130
+
131
+ sections.map do |section|
132
+ if section[:level] <= 1
133
+ nil
134
+ else
135
+ "#{' ' * (section[:level] - 2)}- [#{section[:title]}](##{section[:url]})"
136
+ end
137
+ end.compact.join("\n")
138
+ end
139
+
140
+ def self.generate_individual_documentations!
141
+ Dir['./docs/templates/*.md'].each do |path|
142
+ create_database_and_generate_documentation!(path)
143
+ end
144
+ end
145
+
146
+ def self.create_database_and_generate_documentation!(path)
147
+ puts "> Connecting to Datomic: '#{FLARE_AS_PEER_ADDRESS}'"
148
+
149
+ database_name = "my-datomic-database-docs-#{Flare.uuid.v7}"
150
+
151
+ client = Flare.new(
152
+ credentials: { address: FLARE_AS_PEER_ADDRESS },
153
+ dangerously_override: { database: { name: database_name } }
154
+ )
155
+
156
+ puts "> Creating database: '#{database_name}'"
157
+ client.dsl.create_database!(database_name)
158
+
159
+ begin
160
+ state = { database_name: }
161
+ generate_documentation!(client, state, path)
162
+ ensure
163
+ puts "> Destroying database: '#{database_name}'"
164
+ client.dsl.destroy_database!(database_name)
165
+ end
166
+ end
167
+
168
+ def self.generate_documentation!(client, state, path)
169
+ puts "> Generating documentation: #{path}"
170
+ output = run_and_populate_runnable_codes!(client, state, path)
171
+ output_path = path.sub('docs/templates/', 'docs/')
172
+ File.write(output_path, output)
173
+ puts "> Documentation generated: #{output_path}"
174
+ end
175
+
176
+ def self.run_and_populate_runnable_codes!(client, state, path)
177
+ output = []
178
+ buffer = nil
179
+ next_placeholder = nil
180
+ File.readlines(path).each do |line|
181
+ if line.strip.start_with?('```') && !buffer.nil?
182
+ meta = buffer[:meta].strip.gsub(/`+/, '').split(':')
183
+ language = meta[0]
184
+ tag = meta[1] ? meta[1].split('/')[0] : nil
185
+ action = meta[1] ? meta[1].split('/')[1] : nil
186
+
187
+ output << "```#{language}\n" if tag != 'state' && !['to-request', 'render->to-request'].include?(action)
188
+
189
+ if %w[bash json].include?(language) &&
190
+ tag == 'placeholder' &&
191
+ next_placeholder
192
+ output << if action == 'to-curl'
193
+ to_curl(next_placeholder)
194
+ elsif action == 'to-json'
195
+ to_json(next_placeholder)
196
+ else
197
+ next_placeholder[:formatted]
198
+ end
199
+
200
+ next_placeholder = nil if action != 'to-curl'
201
+ elsif language == 'ruby'
202
+ source_code = buffer[:lines].join
203
+
204
+ if ['render', 'render->to-request'].include?(action)
205
+ source_code = Liquid::Template.parse(source_code).render(
206
+ stringify_keys({ state: })
207
+ )
208
+ end
209
+
210
+ formatted_code = Formatter.format_code(source_code).strip
211
+
212
+ if tag == 'state'
213
+ execution = execute_code(
214
+ client, state,
215
+ next_placeholder ? next_placeholder[:result] : nil,
216
+ formatted_code
217
+ )
218
+ state = execution[:state]
219
+ elsif tag == 'runnable'
220
+ output << formatted_code unless ['to-request', 'render->to-request'].include?(action)
221
+
222
+ if ['to-request', 'render->to-request'].include?(action)
223
+ execution = execute_code(
224
+ client, state.merge({ debug: true }), nil, formatted_code
225
+ )
226
+
227
+ state = execution[:state]
228
+
229
+ next_placeholder = {
230
+ request: execution[:result]
231
+ }
232
+
233
+ execution = execute_code(
234
+ client, state.merge({ debug: false }), nil, formatted_code
235
+ )
236
+
237
+ state = execution[:state]
238
+
239
+ next_placeholder[:result] = execution[:result]
240
+ next_placeholder[:response] = execution[:result]
241
+ else
242
+ execution = execute_code(client, state, nil, formatted_code)
243
+
244
+ state = execution[:state]
245
+
246
+ next_placeholder = { result: execution[:result] }
247
+
248
+ next_placeholder[:formatted] = Formatter.to_s_and_format(
249
+ next_placeholder[:result]
250
+ )
251
+ end
252
+ elsif tag == 'placeholder' && next_placeholder
253
+ output << next_placeholder[:formatted]
254
+ next_placeholder = nil
255
+ else
256
+ output << formatted_code
257
+ end
258
+ else
259
+ output.concat(buffer[:lines])
260
+ end
261
+
262
+ output << "\n```\n" if tag != 'state' && !['to-request', 'render->to-request'].include?(action)
263
+ buffer = nil
264
+ elsif line.strip.start_with?('```') && buffer.nil?
265
+ buffer = { meta: line, lines: [] }
266
+ elsif !buffer.nil?
267
+ buffer[:lines] << line
268
+ else
269
+ output << line
270
+ end
271
+ end
272
+
273
+ output.join
274
+ end
275
+
276
+ def self.to_curl(result)
277
+ http_method = result[:request][:method]
278
+ url = result[:request][:url]
279
+ body = result[:request][:body]
280
+
281
+ body = body&.except('connection')
282
+
283
+ body['database'] = body['database']&.except('name') if body&.key?('database')
284
+
285
+ if body&.key?('inputs')
286
+ body['inputs'] = body['inputs'].map do |input|
287
+ input['database'] = input['database'].except('name') if input.is_a?(Hash) && input.key?('database')
288
+ input
289
+ end
290
+ end
291
+
292
+ curl_command = ''
293
+
294
+ if body && (body.key?('data') || body.key?('query'))
295
+ key = body.key?('data') ? 'data' : 'query'
296
+
297
+ <<~BB.strip
298
+ echo '#{JSON.pretty_generate(body[key])}' | bb -e '(pr-str (edn/read-string (slurp *in*)))'
299
+ BB
300
+
301
+ placeholder = ":CAT#{Flare.uuid.v7}CAT:"
302
+
303
+ edn_value = body[key]
304
+
305
+ body[key] = placeholder
306
+
307
+ curl_command = <<~CURL
308
+ echo '
309
+ #{edn_value.strip}
310
+ ' \\
311
+ | bb -e '(pr-str (edn/read-string (slurp *in*)))' \\
312
+ | curl -s #{url} \\
313
+ -X #{http_method} \\
314
+ -H "Content-Type: application/json" \\
315
+ --data-binary @- <<JSON \\
316
+ | jq
317
+ #{JSON.pretty_generate(body)}
318
+ JSON
319
+ CURL
320
+
321
+ curl_command = curl_command.sub("\"#{placeholder}\"", '$(cat)')
322
+ else
323
+ json_body = body ? JSON.pretty_generate(body) : nil
324
+ curl_command = <<~CURL
325
+ curl -s #{url} \\
326
+ -X #{http_method} \\
327
+ -H "Content-Type: application/json" #{json_body ? "\\\n -d '\n#{json_body}\n'" : ''} \\
328
+ | jq
329
+ CURL
330
+ end
331
+
332
+ curl_command.strip
333
+ end
334
+
335
+ def self.to_json(result)
336
+ data = result[:response].except('meta')
337
+
338
+ placeholder_key = ":JSON#{Flare.uuid.v7}JSON:"
339
+ placeholder_values = []
340
+
341
+ if data['data'].is_a?(Hash) && data['data']['tx-data']
342
+ config = JSON::State.new(
343
+ indent: ' ',
344
+ space: ' ',
345
+ array_nl: '',
346
+ object_nl: ''
347
+ )
348
+ data['data']['tx-data'] = data['data']['tx-data'].each do |array|
349
+ placeholder_values << JSON.generate(array, config).sub('[ ', '[')
350
+ end
351
+ data['data']['tx-data'] = [placeholder_key]
352
+ end
353
+
354
+ output = JSON.pretty_generate(data)
355
+
356
+ output.sub(
357
+ "\"#{placeholder_key}\"",
358
+ placeholder_values.map.with_index do |value, i|
359
+ i.zero? ? value : " #{value}"
360
+ end.join(",\n")
361
+ )
362
+ end
363
+
364
+ def self.execute_code(client, state, result, code)
365
+ context = binding
366
+ context.local_variable_set(:client, client)
367
+ context.local_variable_set(:state, state)
368
+ context.local_variable_set(:result, result)
369
+ result = context.eval(code)
370
+ state = context.eval('state')
371
+ { result:, state: }
372
+ rescue Exception => e
373
+ { result: { error: e.message }, state: }
374
+ end
375
+
376
+ def self.stringify_keys(object)
377
+ result = {}
378
+
379
+ object.each do |key, value|
380
+ string_key = key.to_s
381
+
382
+ result[string_key] = value.is_a?(Hash) ? stringify_keys(value) : value
383
+ end
384
+
385
+ result
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../logic/querying'
4
+
5
+ module Flare
6
+ module DSLQuerying
7
+ def find_by_entity_id(id, database: nil, debug: nil)
8
+ database_input = { latest: true }
9
+
10
+ database_input[:name] = database unless database.nil?
11
+
12
+ response = client.api.entity(
13
+ { database: database_input, id: }, debug:
14
+ )
15
+
16
+ return response if debug
17
+
18
+ QueryingLogic.entity_to_dsl(response['data'])
19
+ end
20
+
21
+ def query(datalog:, params: nil, database: nil, debug: nil)
22
+ database_input = { latest: true }
23
+
24
+ database_input[:name] = database unless database.nil?
25
+
26
+ inputs = [{ database: database_input }]
27
+
28
+ unless params.nil?
29
+ raise "Unexpected params: [#{params.class}] #{params.inspect}" unless params.is_a?(Array)
30
+
31
+ inputs.concat(params)
32
+ end
33
+
34
+ response = client.api.q({ inputs:, query: datalog.strip }, debug:)
35
+
36
+ debug ? response : response['data']
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../logic/schema'
4
+ require_relative '../../logic/types'
5
+
6
+ module Flare
7
+ module DSLSchema
8
+ def transact_schema!(specification, database: nil, debug: nil)
9
+ data = SchemaLogic.specification_to_edn(specification)
10
+
11
+ payload = { data: }
12
+
13
+ payload[:connection] = { database: { name: database } } unless database.nil?
14
+
15
+ response = client.api.transact!(payload, debug:)
16
+
17
+ debug ? response : true
18
+ end
19
+
20
+ def schema(database: nil, debug: nil)
21
+ database_input = { latest: true }
22
+
23
+ database_input[:name] = database unless database.nil?
24
+
25
+ payload = {
26
+ inputs: [{ database: database_input }],
27
+ query: SchemaLogic::QUERY
28
+ }
29
+
30
+ response = client.api.q(payload, debug:)
31
+
32
+ return response if debug
33
+
34
+ SchemaLogic.datoms_to_specification(response['data'])
35
+ end
36
+ end
37
+ end