moku6 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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +131 -0
  4. data/exe/moku6 +6 -0
  5. data/lib/moku6/catalog.rb +23 -0
  6. data/lib/moku6/cli.rb +118 -0
  7. data/lib/moku6/config.rb +61 -0
  8. data/lib/moku6/differ.rb +74 -0
  9. data/lib/moku6/envelope_schema.rb +56 -0
  10. data/lib/moku6/event.rb +54 -0
  11. data/lib/moku6/generate.rb +95 -0
  12. data/lib/moku6/generators/base_generator.rb +31 -0
  13. data/lib/moku6/generators/bigquery_generator.rb +29 -0
  14. data/lib/moku6/generators/cloud_events_generator.rb +47 -0
  15. data/lib/moku6/generators/docs_generator.rb +49 -0
  16. data/lib/moku6/generators/event_catalog_generator.rb +57 -0
  17. data/lib/moku6/generators/example_generator.rb +66 -0
  18. data/lib/moku6/generators/json_schema_generator.rb +23 -0
  19. data/lib/moku6/generators/open_api_generator.rb +60 -0
  20. data/lib/moku6/generators/outbox_generator.rb +96 -0
  21. data/lib/moku6/generators/rails_generator.rb +84 -0
  22. data/lib/moku6/generators/ruby_generator.rb +27 -0
  23. data/lib/moku6/generators/typescript_generator.rb +59 -0
  24. data/lib/moku6/generators.rb +29 -0
  25. data/lib/moku6/initializer.rb +44 -0
  26. data/lib/moku6/linter.rb +60 -0
  27. data/lib/moku6/loader.rb +31 -0
  28. data/lib/moku6/offense.rb +13 -0
  29. data/lib/moku6/reporter.rb +71 -0
  30. data/lib/moku6/result.rb +23 -0
  31. data/lib/moku6/rules/action_naming_rule.rb +18 -0
  32. data/lib/moku6/rules/base_rule.rb +37 -0
  33. data/lib/moku6/rules/example_consistency_rule.rb +53 -0
  34. data/lib/moku6/rules/label_description_rule.rb +21 -0
  35. data/lib/moku6/rules/pii_field_name_heuristic_rule.rb +45 -0
  36. data/lib/moku6/rules/privacy_masking_rule.rb +21 -0
  37. data/lib/moku6/rules/retention_rule.rb +19 -0
  38. data/lib/moku6/rules/schema_rule.rb +31 -0
  39. data/lib/moku6/rules/uniqueness_rule.rb +22 -0
  40. data/lib/moku6/rules/visibility_rule.rb +27 -0
  41. data/lib/moku6/version.rb +6 -0
  42. data/lib/moku6.rb +55 -0
  43. data/schemas/audit-event.schema.json +85 -0
  44. data/sig/generated/moku6/catalog.rbs +22 -0
  45. data/sig/generated/moku6/config.rbs +43 -0
  46. data/sig/generated/moku6/differ.rbs +28 -0
  47. data/sig/generated/moku6/envelope_schema.rbs +19 -0
  48. data/sig/generated/moku6/event.rbs +51 -0
  49. data/sig/generated/moku6/generators/base_generator.rbs +22 -0
  50. data/sig/generated/moku6/generators/bigquery_generator.rbs +12 -0
  51. data/sig/generated/moku6/generators/cloud_events_generator.rbs +19 -0
  52. data/sig/generated/moku6/generators/docs_generator.rbs +18 -0
  53. data/sig/generated/moku6/generators/event_catalog_generator.rbs +16 -0
  54. data/sig/generated/moku6/generators/example_generator.rbs +23 -0
  55. data/sig/generated/moku6/generators/json_schema_generator.rbs +12 -0
  56. data/sig/generated/moku6/generators/open_api_generator.rbs +20 -0
  57. data/sig/generated/moku6/generators/outbox_generator.rbs +23 -0
  58. data/sig/generated/moku6/generators/rails_generator.rbs +23 -0
  59. data/sig/generated/moku6/generators/ruby_generator.rbs +15 -0
  60. data/sig/generated/moku6/generators/typescript_generator.rbs +20 -0
  61. data/sig/generated/moku6/generators.rbs +13 -0
  62. data/sig/generated/moku6/initializer.rbs +23 -0
  63. data/sig/generated/moku6/linter.rbs +31 -0
  64. data/sig/generated/moku6/loader.rbs +15 -0
  65. data/sig/generated/moku6/reporter.rbs +30 -0
  66. data/sig/generated/moku6/result.rbs +22 -0
  67. data/sig/generated/moku6/rules/action_naming_rule.rbs +10 -0
  68. data/sig/generated/moku6/rules/base_rule.rbs +23 -0
  69. data/sig/generated/moku6/rules/example_consistency_rule.rbs +18 -0
  70. data/sig/generated/moku6/rules/label_description_rule.rbs +15 -0
  71. data/sig/generated/moku6/rules/pii_field_name_heuristic_rule.rbs +21 -0
  72. data/sig/generated/moku6/rules/privacy_masking_rule.rbs +10 -0
  73. data/sig/generated/moku6/rules/retention_rule.rbs +10 -0
  74. data/sig/generated/moku6/rules/schema_rule.rbs +15 -0
  75. data/sig/generated/moku6/rules/uniqueness_rule.rbs +11 -0
  76. data/sig/generated/moku6/rules/visibility_rule.rbs +10 -0
  77. data/sig/generated/moku6/version.rbs +5 -0
  78. data/sig/generated/moku6.rbs +9 -0
  79. data/sig/manual/dependencies.rbs +13 -0
  80. data/sig/manual/offense.rbs +13 -0
  81. data/templates/init/.moku6.yml +18 -0
  82. data/templates/init/catalog/employee.updated.yaml +36 -0
  83. metadata +141 -0
@@ -0,0 +1,29 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Moku6
5
+ module Generators
6
+ # Emits BigQuery DDL for the shared envelope (design section 12.5).
7
+ # Fields live in the `metadata JSON` column, consistent with section 8.
8
+ class BigQueryGenerator < BaseGenerator
9
+ #: () -> String
10
+ def render
11
+ <<~SQL
12
+ -- #{AUTOGEN_NOTE}
13
+ CREATE TABLE audit_events (
14
+ event_id STRING NOT NULL,
15
+ action STRING NOT NULL,
16
+ occurred_at TIMESTAMP NOT NULL,
17
+ actor JSON,
18
+ target JSON,
19
+ metadata JSON,
20
+ privacy JSON,
21
+ created_at TIMESTAMP NOT NULL
22
+ )
23
+ PARTITION BY DATE(occurred_at)
24
+ CLUSTER BY action;
25
+ SQL
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "json"
6
+
7
+ module Moku6
8
+ module Generators
9
+ # Emits a CloudEvents (v1.0, structured JSON mode) validation JSON Schema per
10
+ # action. The CloudEvents `type` maps to the action and `data` carries the
11
+ # definition's metadata (design section 13: CloudEvents conversion).
12
+ class CloudEventsGenerator < BaseGenerator
13
+ SPEC_VERSION = "1.0" #: String
14
+
15
+ #: (String dir) -> String
16
+ def write(dir)
17
+ FileUtils.mkdir_p(dir)
18
+ @catalog.sorted.each do |e|
19
+ File.write(File.join(dir, "#{e.action}.json"),
20
+ JSON.pretty_generate(schema_for(e)) + "\n")
21
+ end
22
+ dir
23
+ end
24
+
25
+ private
26
+
27
+ def schema_for(e)
28
+ {
29
+ "$schema" => "https://json-schema.org/draft/2020-12/schema",
30
+ "title" => "#{e.action} (CloudEvents)",
31
+ "type" => "object",
32
+ "required" => %w[specversion id source type data],
33
+ "properties" => {
34
+ "specversion" => {"const" => SPEC_VERSION},
35
+ "id" => {"type" => "string"},
36
+ "source" => {"type" => "string"},
37
+ "type" => {"const" => e.action},
38
+ "subject" => {"type" => "string"},
39
+ "time" => {"type" => "string", "format" => "date-time"},
40
+ "datacontenttype" => {"const" => "application/json"},
41
+ "data" => EnvelopeSchema.metadata_schema(e)
42
+ }
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,49 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Moku6
7
+ module Generators
8
+ class DocsGenerator < BaseGenerator
9
+ #: () -> String
10
+ def render
11
+ lines = ["# Audit Events", "", summary_table, ""]
12
+ @catalog.sorted.each { |e| lines.concat(detail(e)) }
13
+ lines.join("\n")
14
+ end
15
+
16
+ private
17
+
18
+ def summary_table
19
+ rows = @catalog.sorted.map do |e|
20
+ [
21
+ "`#{e.action}`", e.label,
22
+ yn(e.required?),
23
+ yn(e.visibility&.dig("customer_visible")),
24
+ (e.privacy&.dig("contains_personal_data") ? "Yes" : "No"),
25
+ "#{e.retention&.dig("years")} years"
26
+ ]
27
+ end
28
+ header = "| Event | Label | Required | Customer visible | Personal data | Retention |"
29
+ sep = "|---|---|---|---|---|---|"
30
+ ([header, sep] + rows.map { |r| "| #{r.join(" | ")} |" }).join("\n")
31
+ end
32
+
33
+ def detail(e)
34
+ out = ["## #{e.action} — #{e.label}", "", e.description.to_s, ""]
35
+ unless e.fields.empty?
36
+ out << "| Field | Type | Required | Description |"
37
+ out << "|---|---|---|---|"
38
+ e.fields.each do |name, f|
39
+ out << "| #{name} | #{f["type"]} | #{yn(f["required"])} | #{f["description"]} |"
40
+ end
41
+ out << ""
42
+ end
43
+ out
44
+ end
45
+
46
+ def yn(v) = v ? "Yes" : "No"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Moku6
7
+ module Generators
8
+ # Emits EventCatalog (eventcatalog.dev) compatible event documents, one per
9
+ # action at <action>/index.md (design section 13: EventCatalog integration).
10
+ class EventCatalogGenerator < BaseGenerator
11
+ #: (String dir) -> String
12
+ def write(dir)
13
+ FileUtils.mkdir_p(dir)
14
+ @catalog.sorted.each do |e|
15
+ event_dir = File.join(dir, e.action.to_s)
16
+ FileUtils.mkdir_p(event_dir)
17
+ File.write(File.join(event_dir, "index.md"), document(e))
18
+ end
19
+ dir
20
+ end
21
+
22
+ private
23
+
24
+ def document(e)
25
+ out = []
26
+ out << "---"
27
+ out << "id: #{e.action}"
28
+ out << "name: #{e.label}"
29
+ out << "version: #{Moku6::VERSION}"
30
+ out << "summary: #{e.description.to_s.inspect}"
31
+ out << "badges:"
32
+ out << " - content: #{e.category}"
33
+ out << " textColor: blue"
34
+ out << " backgroundColor: blue"
35
+ out << "---"
36
+ out << ""
37
+ out << "<!-- #{AUTOGEN_NOTE} -->"
38
+ out << ""
39
+ out << "## Overview"
40
+ out << ""
41
+ out << e.description.to_s
42
+ out << ""
43
+ unless e.fields.empty?
44
+ out << "## Schema"
45
+ out << ""
46
+ out << "| Field | Type | Required | Description |"
47
+ out << "|---|---|---|---|"
48
+ e.fields.each do |name, f|
49
+ out << "| #{name} | #{f["type"]} | #{f["required"] ? "Yes" : "No"} | #{f["description"]} |"
50
+ end
51
+ out << ""
52
+ end
53
+ out.join("\n")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,66 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "json"
6
+
7
+ module Moku6
8
+ module Generators
9
+ # Generates a minimal sample event (envelope) satisfying required fields per definition.
10
+ class ExampleGenerator < BaseGenerator
11
+ #: (String dir) -> String
12
+ def write(dir)
13
+ FileUtils.mkdir_p(dir)
14
+ @catalog.sorted.each do |e|
15
+ File.write(File.join(dir, "#{e.action}.json"),
16
+ JSON.pretty_generate(example_for(e)) + "\n")
17
+ end
18
+ dir
19
+ end
20
+
21
+ private
22
+
23
+ def example_for(e)
24
+ {
25
+ "event_id" => "00000000-0000-0000-0000-000000000000",
26
+ "action" => e.action,
27
+ "occurred_at" => "2026-01-01T00:00:00Z",
28
+ "actor" => actor_sample(e),
29
+ "target" => target_sample(e),
30
+ "metadata" => metadata_sample(e),
31
+ "privacy" => {"masked_fields" => e.privacy&.dig("masked_fields") || []},
32
+ "created_at" => "2026-01-01T00:00:00Z"
33
+ }
34
+ end
35
+
36
+ def actor_sample(e)
37
+ type = (e.actor.is_a?(Hash) && e.actor["type"]) || "user"
38
+ {"type" => type, "id" => "actor_1"}
39
+ end
40
+
41
+ def target_sample(e)
42
+ type = (e.target.is_a?(Hash) && e.target["type"]) || "target"
43
+ {"type" => type, "id" => "target_1"}
44
+ end
45
+
46
+ def metadata_sample(e)
47
+ e.fields.each_with_object({}) do |(name, f), h|
48
+ next unless f["required"]
49
+
50
+ h[name] = sample_value(f["type"])
51
+ end
52
+ end
53
+
54
+ def sample_value(type)
55
+ case type
56
+ when "integer", "number" then 0
57
+ when "boolean" then false
58
+ when "array" then []
59
+ when "object" then {}
60
+ when "timestamp" then "2026-01-01T00:00:00Z"
61
+ else "example"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "json"
6
+
7
+ module Moku6
8
+ module Generators
9
+ # Emits a runtime event (envelope) validation JSON Schema per action.
10
+ # write(dir) takes a directory and writes multiple files.
11
+ class JsonSchemaGenerator < BaseGenerator
12
+ #: (String dir) -> String
13
+ def write(dir)
14
+ FileUtils.mkdir_p(dir)
15
+ @catalog.sorted.each do |e|
16
+ File.write(File.join(dir, "#{e.action}.json"),
17
+ JSON.pretty_generate(EnvelopeSchema.for(e)) + "\n")
18
+ end
19
+ dir
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+
6
+ module Moku6
7
+ module Generators
8
+ # Emits a single OpenAPI 3.1 document describing audit events as webhooks,
9
+ # with one component schema per action (design section 13: OpenAPI output).
10
+ class OpenApiGenerator < BaseGenerator
11
+ #: () -> String
12
+ def render
13
+ "# #{AUTOGEN_NOTE}\n" + YAML.dump(document)
14
+ end
15
+
16
+ private
17
+
18
+ def document
19
+ {
20
+ "openapi" => "3.1.0",
21
+ "info" => {
22
+ "title" => "Audit Events",
23
+ "version" => Moku6::VERSION,
24
+ "description" => "Audit event webhooks generated by moku6."
25
+ },
26
+ "webhooks" => webhooks,
27
+ "components" => {"schemas" => schemas}
28
+ }
29
+ end
30
+
31
+ def webhooks
32
+ @catalog.sorted.each_with_object({}) do |e, h|
33
+ h[e.action] = {
34
+ "post" => {
35
+ "summary" => e.label.to_s,
36
+ "description" => e.description.to_s,
37
+ "requestBody" => {
38
+ "required" => true,
39
+ "content" => {
40
+ "application/json" => {
41
+ "schema" => {"$ref" => "#/components/schemas/#{e.action}"}
42
+ }
43
+ }
44
+ },
45
+ "responses" => {"200" => {"description" => "Event accepted"}}
46
+ }
47
+ }
48
+ end
49
+ end
50
+
51
+ def schemas
52
+ @catalog.sorted.each_with_object({}) do |e, h|
53
+ schema = EnvelopeSchema.for(e)
54
+ schema.delete("$schema")
55
+ h[e.action] = schema
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Moku6
7
+ module Generators
8
+ # Emits a sample Rails transactional outbox for audit events: a migration,
9
+ # an ActiveRecord model, and a dispatcher (design section 13: Outbox sample).
10
+ # The files are a starting point and are meant to be edited.
11
+ class OutboxGenerator < BaseGenerator
12
+ FILES = {
13
+ "db/migrate/0001_create_audit_event_outbox.rb" => :migration,
14
+ "app/models/audit_event_outbox.rb" => :model,
15
+ "app/services/audit_event_dispatcher.rb" => :dispatcher
16
+ }.freeze #: Hash[String, Symbol]
17
+
18
+ #: (String dir) -> String
19
+ def write(dir)
20
+ FILES.each do |rel, kind|
21
+ path = File.join(dir, rel)
22
+ FileUtils.mkdir_p(File.dirname(path))
23
+ File.write(path, send(kind))
24
+ end
25
+ dir
26
+ end
27
+
28
+ private
29
+
30
+ def migration
31
+ <<~RUBY
32
+ # #{AUTOGEN_NOTE}
33
+ class CreateAuditEventOutbox < ActiveRecord::Migration[7.1]
34
+ def change
35
+ create_table :audit_event_outbox do |t|
36
+ t.string :event_id, null: false
37
+ t.string :action, null: false
38
+ t.jsonb :payload, null: false
39
+ t.datetime :occurred_at, null: false
40
+ t.datetime :dispatched_at
41
+ t.timestamps
42
+ end
43
+
44
+ add_index :audit_event_outbox, :event_id, unique: true
45
+ add_index :audit_event_outbox, :dispatched_at
46
+ end
47
+ end
48
+ RUBY
49
+ end
50
+
51
+ def model
52
+ <<~RUBY
53
+ # frozen_string_literal: true
54
+ # #{AUTOGEN_NOTE}
55
+ class AuditEventOutbox < ApplicationRecord
56
+ self.table_name = "audit_event_outbox"
57
+
58
+ scope :pending, -> { where(dispatched_at: nil) }
59
+
60
+ # Wire this as the AuditEvents sink:
61
+ # AuditEvents.sink = ->(event) { AuditEventOutbox.enqueue(event) }
62
+ def self.enqueue(event)
63
+ create!(
64
+ event_id: event.fetch("event_id"),
65
+ action: event.fetch("action"),
66
+ payload: event,
67
+ occurred_at: event.fetch("occurred_at")
68
+ )
69
+ end
70
+ end
71
+ RUBY
72
+ end
73
+
74
+ def dispatcher
75
+ <<~RUBY
76
+ # frozen_string_literal: true
77
+ # #{AUTOGEN_NOTE}
78
+ # Drains the outbox and forwards each event to a delivery backend
79
+ # (HTTP endpoint, Pub/Sub, Kafka, BigQuery, ...). Run it periodically.
80
+ class AuditEventDispatcher
81
+ def initialize(delivery:)
82
+ @delivery = delivery
83
+ end
84
+
85
+ def run(batch_size: 100)
86
+ AuditEventOutbox.pending.order(:id).limit(batch_size).each do |record|
87
+ @delivery.call(record.payload)
88
+ record.update!(dispatched_at: Time.current)
89
+ end
90
+ end
91
+ end
92
+ RUBY
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,84 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Moku6
5
+ module Generators
6
+ # Emits a Ruby SDK with one typed emitter method per action. Each method
7
+ # builds the canonical envelope and hands it to a configurable sink, so a
8
+ # Rails app can wire it to an outbox (design section 13: Rails SDK).
9
+ class RailsGenerator < BaseGenerator
10
+ #: () -> String
11
+ def render
12
+ out = ["# frozen_string_literal: true", "# #{AUTOGEN_NOTE}", ""]
13
+ out << 'require "securerandom"'
14
+ out << 'require "time"'
15
+ out << ""
16
+ out << "module AuditEvents"
17
+ out.concat(sink_block)
18
+ out << ""
19
+ out << " module Emitter"
20
+ out << " module_function"
21
+ @catalog.sorted.each do |e|
22
+ out << ""
23
+ out.concat(emitter_method(e))
24
+ end
25
+ out << " end"
26
+ out << "end"
27
+ out << ""
28
+ out.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def sink_block
34
+ [
35
+ " # Assign AuditEvents.sink to an object responding to #call(event_hash),",
36
+ " # e.g. an outbox writer. Defaults to returning the event unchanged.",
37
+ " class << self",
38
+ " attr_writer :sink",
39
+ "",
40
+ " def sink",
41
+ " @sink ||= ->(event) { event }",
42
+ " end",
43
+ " end"
44
+ ]
45
+ end
46
+
47
+ def emitter_method(e)
48
+ required = e.fields.select { |_, f| f["required"] }.keys
49
+ optional = e.fields.reject { |_, f| f["required"] }.keys
50
+ params = ["actor:", "target:"]
51
+ params.concat(required.map { |k| "#{k}:" })
52
+ params.concat(optional.map { |k| "#{k}: nil" })
53
+ params << "occurred_at: Time.now.utc"
54
+
55
+ lines = [" def #{method_name(e)}(#{params.join(", ")})"]
56
+ lines << " metadata = {#{required.map { |k| "#{k.inspect} => #{k}" }.join(", ")}}"
57
+ optional.each do |k|
58
+ lines << " metadata[#{k.inspect}] = #{k} unless #{k}.nil?"
59
+ end
60
+ lines << " event = {"
61
+ lines << ' "event_id" => SecureRandom.uuid,'
62
+ lines << " \"action\" => #{e.action.inspect},"
63
+ lines << ' "occurred_at" => occurred_at.iso8601,'
64
+ lines << ' "actor" => actor,'
65
+ lines << ' "target" => target,'
66
+ lines << ' "metadata" => metadata,'
67
+ lines << " \"privacy\" => {\"masked_fields\" => #{masked_fields(e).inspect}},"
68
+ lines << ' "created_at" => Time.now.utc.iso8601'
69
+ lines << " }"
70
+ lines << " AuditEvents.sink.call(event)"
71
+ lines << " end"
72
+ lines
73
+ end
74
+
75
+ def method_name(e)
76
+ e.action.split(/[._]/).join("_")
77
+ end
78
+
79
+ def masked_fields(e)
80
+ (e.privacy.is_a?(Hash) && e.privacy["masked_fields"]) || []
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Moku6
5
+ module Generators
6
+ # Emits Ruby constants for every action (design section 12.4).
7
+ class RubyGenerator < BaseGenerator
8
+ #: () -> String
9
+ def render
10
+ consts = @catalog.sorted.map { |e| [const_name(e), e.action] }
11
+ out = ["# frozen_string_literal: true", "# #{AUTOGEN_NOTE}", "module AuditEvents"]
12
+ consts.each { |name, action| out << " #{name} = #{action.inspect}" }
13
+ out << ""
14
+ out << " ALL = [#{consts.map(&:first).join(", ")}].freeze"
15
+ out << "end"
16
+ out << ""
17
+ out.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ def const_name(e)
23
+ e.action.split(/[._]/).join("_").upcase
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Moku6
5
+ module Generators
6
+ class TypeScriptGenerator < BaseGenerator
7
+ #: () -> String
8
+ def render
9
+ events = @catalog.sorted
10
+ out = ["// #{AUTOGEN_NOTE}", ""]
11
+ out.concat(union_type(events))
12
+ out << ""
13
+ events.each { |e| out.concat(interface(e)) }
14
+ out << "export interface AuditEventMetadataMap {"
15
+ events.each { |e| out << " \"#{e.action}\": #{iface_name(e)};" }
16
+ out << "}"
17
+ out << ""
18
+ out.join("\n")
19
+ end
20
+
21
+ private
22
+
23
+ def union_type(events)
24
+ return ["export type AuditAction = never;"] if events.empty?
25
+
26
+ lines = ["export type AuditAction ="]
27
+ events.each_with_index do |e, i|
28
+ suffix = (i == events.size - 1) ? ";" : ""
29
+ lines << " | \"#{e.action}\"#{suffix}"
30
+ end
31
+ lines
32
+ end
33
+
34
+ def interface(e)
35
+ lines = ["export interface #{iface_name(e)} {"]
36
+ e.fields.each do |name, f|
37
+ opt = f["required"] ? "" : "?"
38
+ lines << " #{name}#{opt}: #{ts(f)};"
39
+ end
40
+ lines << "}" << ""
41
+ lines
42
+ end
43
+
44
+ def iface_name(e)
45
+ e.action.split(/[._]/).map(&:capitalize).join + "Metadata"
46
+ end
47
+
48
+ def ts(f)
49
+ case f["type"]
50
+ when "integer", "number" then "number"
51
+ when "boolean" then "boolean"
52
+ when "array" then "unknown[]"
53
+ when "object" then "Record<string, unknown>"
54
+ else "string"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Moku6
5
+ module Generators
6
+ REGISTRY = {
7
+ docs: DocsGenerator,
8
+ json_schema: JsonSchemaGenerator,
9
+ typescript: TypeScriptGenerator,
10
+ ruby: RubyGenerator,
11
+ bigquery: BigQueryGenerator,
12
+ cloudevents: CloudEventsGenerator,
13
+ eventcatalog: EventCatalogGenerator,
14
+ openapi: OpenApiGenerator,
15
+ rails: RailsGenerator,
16
+ outbox: OutboxGenerator,
17
+ example: ExampleGenerator
18
+ } #: Hash[Symbol, untyped]
19
+
20
+ #: (String | Symbol name, untyped klass) -> untyped
21
+ def self.register(name, klass) = REGISTRY[name.to_sym] = klass
22
+
23
+ #: (String | Symbol name, Catalog catalog, Config config) -> untyped
24
+ def self.build(name, catalog, config)
25
+ klass = REGISTRY.fetch(name.to_sym) { raise UsageError, "unknown generator: #{name}" }
26
+ klass.new(catalog, config)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Moku6
7
+ # Backs `moku6 init`. Copies templates/init into ./ (never overwrites existing files).
8
+ class Initializer
9
+ TEMPLATE_DIR = File.expand_path("../../templates/init", __dir__.to_s) #: String
10
+
11
+ # @rbs @options: Hash[Symbol, untyped]
12
+ # @rbs @dest_root: String
13
+
14
+ #: (?Hash[Symbol, untyped] options) -> void
15
+ def initialize(options = {})
16
+ @options = options
17
+ @dest_root = Dir.pwd
18
+ end
19
+
20
+ #: () -> void
21
+ def run
22
+ entries.each do |src|
23
+ rel = src.delete_prefix("#{TEMPLATE_DIR}/")
24
+ dest = File.join(@dest_root, rel)
25
+ if File.exist?(dest)
26
+ puts "skip (exists): #{rel}"
27
+ next
28
+ end
29
+ FileUtils.mkdir_p(File.dirname(dest))
30
+ FileUtils.cp(src, dest)
31
+ puts "create: #{rel}"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ #: () -> Array[String]
38
+ def entries
39
+ Dir.glob(File.join(TEMPLATE_DIR, "**", "*"), File::FNM_DOTMATCH)
40
+ .reject { |p| File.directory?(p) }
41
+ .sort
42
+ end
43
+ end
44
+ end