igniter-extensions 0.5.2
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 +7 -0
- data/README.md +381 -0
- data/lib/igniter/extensions/contracts/aggregate_pack.rb +103 -0
- data/lib/igniter/extensions/contracts/audit/builder.rb +132 -0
- data/lib/igniter/extensions/contracts/audit/event.rb +34 -0
- data/lib/igniter/extensions/contracts/audit/snapshot.rb +44 -0
- data/lib/igniter/extensions/contracts/audit_pack.rb +60 -0
- data/lib/igniter/extensions/contracts/branch_pack.rb +199 -0
- data/lib/igniter/extensions/contracts/capabilities/declaration.rb +31 -0
- data/lib/igniter/extensions/contracts/capabilities/error.rb +35 -0
- data/lib/igniter/extensions/contracts/capabilities/policy.rb +20 -0
- data/lib/igniter/extensions/contracts/capabilities/report.rb +47 -0
- data/lib/igniter/extensions/contracts/capabilities/violation.rb +30 -0
- data/lib/igniter/extensions/contracts/capabilities_pack.rb +146 -0
- data/lib/igniter/extensions/contracts/collection_pack.rb +212 -0
- data/lib/igniter/extensions/contracts/commerce_pack.rb +91 -0
- data/lib/igniter/extensions/contracts/compose_pack.rb +213 -0
- data/lib/igniter/extensions/contracts/content_addressing/cache.rb +59 -0
- data/lib/igniter/extensions/contracts/content_addressing/content_key.rb +63 -0
- data/lib/igniter/extensions/contracts/content_addressing/declaration.rb +47 -0
- data/lib/igniter/extensions/contracts/content_addressing_pack.rb +90 -0
- data/lib/igniter/extensions/contracts/creator/profile.rb +196 -0
- data/lib/igniter/extensions/contracts/creator/report.rb +85 -0
- data/lib/igniter/extensions/contracts/creator/scaffold.rb +461 -0
- data/lib/igniter/extensions/contracts/creator/scope.rb +79 -0
- data/lib/igniter/extensions/contracts/creator/wizard.rb +269 -0
- data/lib/igniter/extensions/contracts/creator/workflow.rb +189 -0
- data/lib/igniter/extensions/contracts/creator/workflow_step.rb +51 -0
- data/lib/igniter/extensions/contracts/creator/write_result.rb +48 -0
- data/lib/igniter/extensions/contracts/creator/write_step.rb +63 -0
- data/lib/igniter/extensions/contracts/creator/writer.rb +131 -0
- data/lib/igniter/extensions/contracts/creator_pack.rb +128 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_operators.rb +119 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_state.rb +60 -0
- data/lib/igniter/extensions/contracts/dataflow/builder.rb +66 -0
- data/lib/igniter/extensions/contracts/dataflow/collection_result.rb +70 -0
- data/lib/igniter/extensions/contracts/dataflow/diff.rb +37 -0
- data/lib/igniter/extensions/contracts/dataflow/item_result.rb +44 -0
- data/lib/igniter/extensions/contracts/dataflow/result.rb +58 -0
- data/lib/igniter/extensions/contracts/dataflow/session.rb +173 -0
- data/lib/igniter/extensions/contracts/dataflow/window_filter.rb +49 -0
- data/lib/igniter/extensions/contracts/dataflow_pack.rb +66 -0
- data/lib/igniter/extensions/contracts/debug/pack_audit.rb +181 -0
- data/lib/igniter/extensions/contracts/debug/pack_snapshot.rb +46 -0
- data/lib/igniter/extensions/contracts/debug/profile_snapshot.rb +50 -0
- data/lib/igniter/extensions/contracts/debug/report.rb +50 -0
- data/lib/igniter/extensions/contracts/debug_pack.rb +115 -0
- data/lib/igniter/extensions/contracts/differential/divergence.rb +37 -0
- data/lib/igniter/extensions/contracts/differential/formatter.rb +85 -0
- data/lib/igniter/extensions/contracts/differential/report.rb +83 -0
- data/lib/igniter/extensions/contracts/differential/runner.rb +136 -0
- data/lib/igniter/extensions/contracts/differential_pack.rb +61 -0
- data/lib/igniter/extensions/contracts/execution_report_pack.rb +38 -0
- data/lib/igniter/extensions/contracts/incremental/formatter.rb +60 -0
- data/lib/igniter/extensions/contracts/incremental/node_state.rb +30 -0
- data/lib/igniter/extensions/contracts/incremental/result.rb +65 -0
- data/lib/igniter/extensions/contracts/incremental/session.rb +146 -0
- data/lib/igniter/extensions/contracts/incremental_pack.rb +40 -0
- data/lib/igniter/extensions/contracts/invariants/builder.rb +27 -0
- data/lib/igniter/extensions/contracts/invariants/cases_report.rb +47 -0
- data/lib/igniter/extensions/contracts/invariants/error.rb +34 -0
- data/lib/igniter/extensions/contracts/invariants/invariant.rb +30 -0
- data/lib/igniter/extensions/contracts/invariants/report.rb +45 -0
- data/lib/igniter/extensions/contracts/invariants/suite.rb +36 -0
- data/lib/igniter/extensions/contracts/invariants/violation.rb +39 -0
- data/lib/igniter/extensions/contracts/invariants_pack.rb +88 -0
- data/lib/igniter/extensions/contracts/journal_pack.rb +55 -0
- data/lib/igniter/extensions/contracts/language/formula_pack.rb +185 -0
- data/lib/igniter/extensions/contracts/language/piecewise_pack.rb +166 -0
- data/lib/igniter/extensions/contracts/language/scale_pack.rb +147 -0
- data/lib/igniter/extensions/contracts/lookup_pack.rb +50 -0
- data/lib/igniter/extensions/contracts/mcp/creator_session.rb +105 -0
- data/lib/igniter/extensions/contracts/mcp/tool_argument.rb +35 -0
- data/lib/igniter/extensions/contracts/mcp/tool_definition.rb +33 -0
- data/lib/igniter/extensions/contracts/mcp/tool_result.rb +28 -0
- data/lib/igniter/extensions/contracts/mcp_pack.rb +335 -0
- data/lib/igniter/extensions/contracts/provenance/builder.rb +80 -0
- data/lib/igniter/extensions/contracts/provenance/lineage.rb +59 -0
- data/lib/igniter/extensions/contracts/provenance/node_trace.rb +53 -0
- data/lib/igniter/extensions/contracts/provenance/text_formatter.rb +62 -0
- data/lib/igniter/extensions/contracts/provenance_pack.rb +52 -0
- data/lib/igniter/extensions/contracts/reactive/builder.rb +43 -0
- data/lib/igniter/extensions/contracts/reactive/dispatch_result.rb +59 -0
- data/lib/igniter/extensions/contracts/reactive/engine.rb +79 -0
- data/lib/igniter/extensions/contracts/reactive/event.rb +36 -0
- data/lib/igniter/extensions/contracts/reactive/matcher.rb +20 -0
- data/lib/igniter/extensions/contracts/reactive/plan.rb +58 -0
- data/lib/igniter/extensions/contracts/reactive/subscription.rb +29 -0
- data/lib/igniter/extensions/contracts/reactive_pack.rb +169 -0
- data/lib/igniter/extensions/contracts/saga/compensation.rb +25 -0
- data/lib/igniter/extensions/contracts/saga/compensation_record.rb +28 -0
- data/lib/igniter/extensions/contracts/saga/compensation_set.rb +47 -0
- data/lib/igniter/extensions/contracts/saga/formatter.rb +39 -0
- data/lib/igniter/extensions/contracts/saga/result.rb +56 -0
- data/lib/igniter/extensions/contracts/saga/runner.rb +124 -0
- data/lib/igniter/extensions/contracts/saga_pack.rb +56 -0
- data/lib/igniter/extensions/contracts.rb +445 -0
- data/lib/igniter/extensions.rb +6 -0
- data/lib/igniter-extensions.rb +3 -0
- metadata +152 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require_relative "write_step"
|
|
6
|
+
require_relative "write_result"
|
|
7
|
+
|
|
8
|
+
module Igniter
|
|
9
|
+
module Extensions
|
|
10
|
+
module Contracts
|
|
11
|
+
module Creator
|
|
12
|
+
class Writer
|
|
13
|
+
VALID_MODES = %i[skip_existing overwrite].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :workflow, :root, :mode
|
|
16
|
+
|
|
17
|
+
def initialize(workflow:, root:, mode: :skip_existing)
|
|
18
|
+
@workflow = workflow
|
|
19
|
+
@root = File.expand_path(root.to_s)
|
|
20
|
+
@mode = normalize_mode(mode)
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def scaffold
|
|
25
|
+
workflow.scaffold
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def plan
|
|
29
|
+
WriteResult.new(root: root, mode: mode, steps: plan_steps)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write
|
|
33
|
+
WriteResult.new(root: root, mode: mode, steps: directory_steps + file_steps)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def normalize_mode(mode)
|
|
39
|
+
value = mode.to_sym
|
|
40
|
+
return value if VALID_MODES.include?(value)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, "unsupported creator writer mode #{mode.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def plan_steps
|
|
46
|
+
planned_directories + planned_files
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def planned_directories
|
|
50
|
+
directories.map do |relative_path|
|
|
51
|
+
WriteStep.new(
|
|
52
|
+
kind: :directory,
|
|
53
|
+
relative_path: relative_path,
|
|
54
|
+
absolute_path: absolute_path_for(relative_path),
|
|
55
|
+
status: File.directory?(absolute_path_for(relative_path)) ? :unchanged : :pending
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def planned_files
|
|
61
|
+
scaffold.files.map do |relative_path, _content|
|
|
62
|
+
absolute_path = absolute_path_for(relative_path)
|
|
63
|
+
WriteStep.new(
|
|
64
|
+
kind: :file,
|
|
65
|
+
relative_path: relative_path,
|
|
66
|
+
absolute_path: absolute_path,
|
|
67
|
+
status: file_plan_status(absolute_path)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def directory_steps
|
|
73
|
+
directories.map do |relative_path|
|
|
74
|
+
absolute_path = absolute_path_for(relative_path)
|
|
75
|
+
existed = File.directory?(absolute_path)
|
|
76
|
+
FileUtils.mkdir_p(absolute_path)
|
|
77
|
+
WriteStep.new(
|
|
78
|
+
kind: :directory,
|
|
79
|
+
relative_path: relative_path,
|
|
80
|
+
absolute_path: absolute_path,
|
|
81
|
+
status: existed ? :unchanged : :created
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def file_steps
|
|
87
|
+
scaffold.files.map do |relative_path, content|
|
|
88
|
+
absolute_path = absolute_path_for(relative_path)
|
|
89
|
+
parent = File.dirname(absolute_path)
|
|
90
|
+
FileUtils.mkdir_p(parent)
|
|
91
|
+
|
|
92
|
+
if File.exist?(absolute_path) && mode == :skip_existing
|
|
93
|
+
WriteStep.new(
|
|
94
|
+
kind: :file,
|
|
95
|
+
relative_path: relative_path,
|
|
96
|
+
absolute_path: absolute_path,
|
|
97
|
+
status: :skipped,
|
|
98
|
+
reason: "existing file preserved"
|
|
99
|
+
)
|
|
100
|
+
else
|
|
101
|
+
status = File.exist?(absolute_path) && File.read(absolute_path) == content ? :unchanged : :written
|
|
102
|
+
File.write(absolute_path, content)
|
|
103
|
+
WriteStep.new(
|
|
104
|
+
kind: :file,
|
|
105
|
+
relative_path: relative_path,
|
|
106
|
+
absolute_path: absolute_path,
|
|
107
|
+
status: status
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def file_plan_status(absolute_path)
|
|
114
|
+
return :pending unless File.exist?(absolute_path)
|
|
115
|
+
return :skipped if mode == :skip_existing
|
|
116
|
+
|
|
117
|
+
:pending
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def directories
|
|
121
|
+
scaffold.files.keys.map { |path| File.dirname(path) }.uniq.sort
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def absolute_path_for(relative_path)
|
|
125
|
+
File.join(root, relative_path)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "creator/profile"
|
|
4
|
+
require_relative "creator/scaffold"
|
|
5
|
+
require_relative "creator/scope"
|
|
6
|
+
require_relative "creator/report"
|
|
7
|
+
require_relative "creator/workflow"
|
|
8
|
+
require_relative "creator/wizard"
|
|
9
|
+
|
|
10
|
+
module Igniter
|
|
11
|
+
module Extensions
|
|
12
|
+
module Contracts
|
|
13
|
+
module CreatorPack
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def manifest
|
|
17
|
+
Igniter::Contracts::PackManifest.new(
|
|
18
|
+
name: :extensions_creator,
|
|
19
|
+
requires_packs: [DebugPack],
|
|
20
|
+
metadata: { category: :developer }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def install_into(kernel)
|
|
25
|
+
kernel
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def available_profiles
|
|
29
|
+
Creator::Profile.available
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def available_scopes
|
|
33
|
+
Creator::Scope.available
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scaffold(name:, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
37
|
+
scope: :monorepo_package)
|
|
38
|
+
authoring_profile = Creator::Profile.build(profile: profile, kind: kind, capabilities: capabilities)
|
|
39
|
+
target_scope = Creator::Scope.build(scope)
|
|
40
|
+
Creator::Scaffold.new(
|
|
41
|
+
name: name,
|
|
42
|
+
kind: authoring_profile.kind,
|
|
43
|
+
namespace: namespace,
|
|
44
|
+
profile: authoring_profile,
|
|
45
|
+
scope: target_scope
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def report(name:, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
50
|
+
scope: :monorepo_package, pack: nil, target_profile: nil)
|
|
51
|
+
generated = scaffold(
|
|
52
|
+
name: name,
|
|
53
|
+
kind: kind,
|
|
54
|
+
namespace: namespace,
|
|
55
|
+
profile: profile,
|
|
56
|
+
capabilities: capabilities,
|
|
57
|
+
scope: scope
|
|
58
|
+
)
|
|
59
|
+
audit = pack ? DebugPack.audit(pack, profile: target_profile) : nil
|
|
60
|
+
|
|
61
|
+
Creator::Report.new(scaffold: generated, audit: audit)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def workflow(name:, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
65
|
+
scope: :monorepo_package, pack: nil, target_profile: nil)
|
|
66
|
+
Creator::Workflow.new(
|
|
67
|
+
report: report(
|
|
68
|
+
name: name,
|
|
69
|
+
kind: kind,
|
|
70
|
+
namespace: namespace,
|
|
71
|
+
profile: profile,
|
|
72
|
+
capabilities: capabilities,
|
|
73
|
+
scope: scope,
|
|
74
|
+
pack: pack,
|
|
75
|
+
target_profile: target_profile
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def wizard(name: nil, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
81
|
+
scope: nil, root: nil, mode: :skip_existing, pack: nil, target_profile: nil)
|
|
82
|
+
Creator::Wizard.new(
|
|
83
|
+
name: name,
|
|
84
|
+
kind: kind,
|
|
85
|
+
namespace: namespace,
|
|
86
|
+
profile: profile,
|
|
87
|
+
capabilities: capabilities,
|
|
88
|
+
scope: scope,
|
|
89
|
+
root: root,
|
|
90
|
+
mode: mode,
|
|
91
|
+
pack: pack,
|
|
92
|
+
target_profile: target_profile
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def writer(name:, root:, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
97
|
+
scope: :monorepo_package, pack: nil, target_profile: nil, mode: :skip_existing)
|
|
98
|
+
workflow(
|
|
99
|
+
name: name,
|
|
100
|
+
kind: kind,
|
|
101
|
+
namespace: namespace,
|
|
102
|
+
profile: profile,
|
|
103
|
+
capabilities: capabilities,
|
|
104
|
+
scope: scope,
|
|
105
|
+
pack: pack,
|
|
106
|
+
target_profile: target_profile
|
|
107
|
+
).writer(root: root, mode: mode)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def write(name:, root:, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
|
|
111
|
+
scope: :monorepo_package, pack: nil, target_profile: nil, mode: :skip_existing)
|
|
112
|
+
writer(
|
|
113
|
+
name: name,
|
|
114
|
+
kind: kind,
|
|
115
|
+
namespace: namespace,
|
|
116
|
+
profile: profile,
|
|
117
|
+
capabilities: capabilities,
|
|
118
|
+
scope: scope,
|
|
119
|
+
pack: pack,
|
|
120
|
+
target_profile: target_profile,
|
|
121
|
+
root: root,
|
|
122
|
+
mode: mode
|
|
123
|
+
).write
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
module AggregateOperators
|
|
8
|
+
Operator = Struct.new(
|
|
9
|
+
:initial_fn, :project, :add, :remove, :finalize, :recompute,
|
|
10
|
+
keyword_init: true
|
|
11
|
+
) do
|
|
12
|
+
def initial
|
|
13
|
+
initial_fn.call
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def count(filter: nil)
|
|
20
|
+
Operator.new(
|
|
21
|
+
initial_fn: -> { 0 },
|
|
22
|
+
project: ->(item) { filter.nil? || filter.call(item) ? 1 : 0 },
|
|
23
|
+
add: ->(acc, value) { acc + value },
|
|
24
|
+
remove: ->(acc, value) { acc - value },
|
|
25
|
+
finalize: ->(acc, _) { acc },
|
|
26
|
+
recompute: false
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sum(projection:)
|
|
31
|
+
Operator.new(
|
|
32
|
+
initial_fn: -> { 0 },
|
|
33
|
+
project: ->(item) { project(item, projection).to_f },
|
|
34
|
+
add: ->(acc, value) { acc + value },
|
|
35
|
+
remove: ->(acc, value) { acc - value },
|
|
36
|
+
finalize: ->(acc, _) { acc },
|
|
37
|
+
recompute: false
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def avg(projection:)
|
|
42
|
+
Operator.new(
|
|
43
|
+
initial_fn: -> { { sum: 0.0, count: 0 } },
|
|
44
|
+
project: ->(item) { project(item, projection).to_f },
|
|
45
|
+
add: ->(acc, value) { { sum: acc.fetch(:sum) + value, count: acc.fetch(:count) + 1 } },
|
|
46
|
+
remove: ->(acc, value) { { sum: acc.fetch(:sum) - value, count: acc.fetch(:count) - 1 } },
|
|
47
|
+
finalize: lambda { |acc, _|
|
|
48
|
+
acc.fetch(:count).zero? ? 0.0 : acc.fetch(:sum) / acc.fetch(:count)
|
|
49
|
+
},
|
|
50
|
+
recompute: false
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def min(projection:)
|
|
55
|
+
recomputed_projection(projection, &:min)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def max(projection:)
|
|
59
|
+
recomputed_projection(projection, &:max)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def group_count(projection:)
|
|
63
|
+
Operator.new(
|
|
64
|
+
initial_fn: -> { {} },
|
|
65
|
+
project: ->(item) { project(item, projection) },
|
|
66
|
+
add: ->(acc, group_key) { acc.merge(group_key => (acc[group_key] || 0) + 1) },
|
|
67
|
+
remove: lambda { |acc, group_key|
|
|
68
|
+
count = (acc[group_key] || 1) - 1
|
|
69
|
+
count <= 0 ? acc.reject { |key, _| key == group_key } : acc.merge(group_key => count)
|
|
70
|
+
},
|
|
71
|
+
finalize: ->(acc, _) { acc },
|
|
72
|
+
recompute: false
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def custom(initial:, add:, remove:)
|
|
77
|
+
Operator.new(
|
|
78
|
+
initial_fn: -> { duplicate(initial) },
|
|
79
|
+
project: ->(item) { item },
|
|
80
|
+
add: ->(acc, item) { add.call(acc, item) },
|
|
81
|
+
remove: ->(acc, item) { remove.call(acc, item) },
|
|
82
|
+
finalize: ->(acc, _) { acc },
|
|
83
|
+
recompute: false
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def project(item, projection)
|
|
88
|
+
return item if projection.nil?
|
|
89
|
+
return projection.call(item) if projection.respond_to?(:call)
|
|
90
|
+
|
|
91
|
+
key = projection.to_sym
|
|
92
|
+
return item.output(key) if item.outputs.key?(key)
|
|
93
|
+
return item.input(key) if item.inputs.key?(key)
|
|
94
|
+
|
|
95
|
+
raise KeyError,
|
|
96
|
+
"aggregate projection #{projection.inspect} not present on dataflow item #{item.key.inspect}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def recomputed_projection(projection, &finalizer)
|
|
100
|
+
Operator.new(
|
|
101
|
+
initial_fn: -> { nil },
|
|
102
|
+
project: ->(item) { project(item, projection) },
|
|
103
|
+
add: nil,
|
|
104
|
+
remove: nil,
|
|
105
|
+
finalize: ->(_acc, contributions) { finalizer.call(contributions.values) },
|
|
106
|
+
recompute: true
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def duplicate(value)
|
|
111
|
+
value.frozen? ? value : value.dup
|
|
112
|
+
rescue TypeError
|
|
113
|
+
value
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class AggregateState
|
|
8
|
+
def initialize(operator)
|
|
9
|
+
@operator = operator
|
|
10
|
+
@contributions = {}
|
|
11
|
+
@accum = operator.initial
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def apply_diff!(diff, collection_result)
|
|
15
|
+
diff.changed.each do |key|
|
|
16
|
+
retract!(key)
|
|
17
|
+
contribute!(key, collection_result[key])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
diff.added.each do |key|
|
|
21
|
+
contribute!(key, collection_result[key])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
diff.removed.each do |key|
|
|
25
|
+
retract!(key)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def value
|
|
30
|
+
if @operator.recompute
|
|
31
|
+
@operator.finalize.call(nil, @contributions)
|
|
32
|
+
else
|
|
33
|
+
@operator.finalize.call(@accum, @contributions.size)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def contribute!(key, item)
|
|
40
|
+
contribution = @operator.project.call(item)
|
|
41
|
+
return if contribution.nil?
|
|
42
|
+
|
|
43
|
+
@contributions[key] = contribution
|
|
44
|
+
return if @operator.recompute
|
|
45
|
+
|
|
46
|
+
@accum = @operator.add.call(@accum, contribution)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def retract!(key)
|
|
50
|
+
old_contribution = @contributions.delete(key)
|
|
51
|
+
return unless old_contribution
|
|
52
|
+
return if @operator.recompute
|
|
53
|
+
|
|
54
|
+
@accum = @operator.remove.call(@accum, old_contribution)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class Builder
|
|
8
|
+
attr_reader :source, :key, :window, :context
|
|
9
|
+
|
|
10
|
+
def initialize(source:, key:, window: nil, context: [])
|
|
11
|
+
@source = source.to_sym
|
|
12
|
+
@key = key.to_sym
|
|
13
|
+
@window = window
|
|
14
|
+
@context = Array(context).map(&:to_sym).freeze
|
|
15
|
+
@item_block = nil
|
|
16
|
+
@aggregate_operators = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def item(&block)
|
|
20
|
+
@item_block = block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def count(name, matching: nil)
|
|
24
|
+
register(name, AggregateOperators.count(filter: matching))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sum(name, using:)
|
|
28
|
+
register(name, AggregateOperators.sum(projection: using))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def avg(name, using:)
|
|
32
|
+
register(name, AggregateOperators.avg(projection: using))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def min(name, using:)
|
|
36
|
+
register(name, AggregateOperators.min(projection: using))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def max(name, using:)
|
|
40
|
+
register(name, AggregateOperators.max(projection: using))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def group_count(name, using:)
|
|
44
|
+
register(name, AggregateOperators.group_count(projection: using))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def aggregate(name, initial:, add:, remove:)
|
|
48
|
+
register(name, AggregateOperators.custom(initial: initial, add: add, remove: remove))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build!(environment)
|
|
52
|
+
raise Igniter::Contracts::Error, "DataflowPack requires an `item do ... end` definition" unless @item_block
|
|
53
|
+
|
|
54
|
+
[environment.compile(&@item_block), @aggregate_operators.dup.freeze]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def register(name, operator)
|
|
60
|
+
@aggregate_operators[name.to_sym] = operator
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class CollectionResult
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
attr_reader :items, :diff
|
|
11
|
+
|
|
12
|
+
def initialize(items:, diff:)
|
|
13
|
+
@items = items.dup.freeze
|
|
14
|
+
@diff = diff
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](key)
|
|
19
|
+
items[key]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(key)
|
|
23
|
+
items.fetch(key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each(&block)
|
|
27
|
+
items.each(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each_value(&block)
|
|
31
|
+
items.each_value(&block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def keys
|
|
35
|
+
items.keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def values
|
|
39
|
+
items.values
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def successes
|
|
43
|
+
items
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def summary
|
|
47
|
+
{
|
|
48
|
+
mode: :incremental,
|
|
49
|
+
total: items.size,
|
|
50
|
+
succeeded: items.size,
|
|
51
|
+
failed: 0,
|
|
52
|
+
status: :success,
|
|
53
|
+
added: diff.added.size,
|
|
54
|
+
removed: diff.removed.size,
|
|
55
|
+
changed: diff.changed.size,
|
|
56
|
+
unchanged: diff.unchanged.size
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{
|
|
62
|
+
items: items.transform_values(&:to_h),
|
|
63
|
+
diff: diff.to_h
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
Diff = Struct.new(:added, :removed, :changed, :unchanged, keyword_init: true) do
|
|
8
|
+
def any_changes?
|
|
9
|
+
added.any? || removed.any? || changed.any?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def processed_count
|
|
13
|
+
added.size + changed.size
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def explain
|
|
17
|
+
parts = []
|
|
18
|
+
parts << "added(#{added.size}): #{added.inspect}" unless added.empty?
|
|
19
|
+
parts << "removed(#{removed.size}): #{removed.inspect}" unless removed.empty?
|
|
20
|
+
parts << "changed(#{changed.size}): #{changed.inspect}" unless changed.empty?
|
|
21
|
+
parts << "unchanged(#{unchanged.size})" unless unchanged.empty?
|
|
22
|
+
parts.empty? ? "(no changes)" : parts.join(", ")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
added: added,
|
|
28
|
+
removed: removed,
|
|
29
|
+
changed: changed,
|
|
30
|
+
unchanged: unchanged
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class ItemResult
|
|
8
|
+
attr_reader :key, :inputs, :execution_result, :incremental_result
|
|
9
|
+
|
|
10
|
+
def initialize(key:, inputs:, execution_result:, incremental_result:)
|
|
11
|
+
@key = key
|
|
12
|
+
@inputs = Igniter::Contracts::NamedValues.new(inputs)
|
|
13
|
+
@execution_result = execution_result
|
|
14
|
+
@incremental_result = incremental_result
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
alias result execution_result
|
|
19
|
+
|
|
20
|
+
def input(name)
|
|
21
|
+
inputs.fetch(name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def output(name)
|
|
25
|
+
execution_result.output(name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def outputs
|
|
29
|
+
execution_result.outputs
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
key: key,
|
|
35
|
+
inputs: inputs.to_h,
|
|
36
|
+
execution_result: execution_result.to_h,
|
|
37
|
+
incremental_result: incremental_result.to_h
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|