klue-langcraft 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c27050239df45678f9bf8b8527c20f033532235d023cf22bd255b06f0f48b38
4
- data.tar.gz: 22e0fab109f694eb0fc501885f38e94b8e0eae55da1e66381093fc8cedbece86
3
+ metadata.gz: '07528e25c004b36664752d235737a9f6f521e231eb2b9e900884861e2fbeccad'
4
+ data.tar.gz: bd96ac7b1247f8f4fe123b4bb7923df50f1067120b1faa6e1121c6936fe400d1
5
5
  SHA512:
6
- metadata.gz: 78d038c2e19671fc645c02215820345d9a89f8552869f9eb2f0d0f6769e348bef56f218ff39114bd6f8945ef44001d928b1f5a0f4896e8510ffcebe6ad9f49a3
7
- data.tar.gz: 80e94051e6ad5104cba4b199b472bbea3f62bdc23903a71974fb04d9399a92beb23627581ef30c9e02a0f8a821d1a9ef48cc9cb7a19a1cd223a1af95085b9c1c
6
+ metadata.gz: f270af8583b0ef01f5c290f0d69abe2de5534682169440381532fea9510c0b2b03f7c247c13dcde3fd13dc88b89fdadc3255af804138b6373513ea16a32b31d6
7
+ data.tar.gz: e23279553bc9f644ebe56ce796dd99cda278c10170142806e2a9ed59839a3a51a541de9b0b75082474e46f130cd6e3e5d037d2c5e9c475585280ba032d84a6d8
data/.rubocop.yml CHANGED
@@ -14,6 +14,7 @@ AllCops:
14
14
  Exclude:
15
15
  - ".builders/**/*"
16
16
  - "spec/samples/**/*"
17
+ - "lib/*" # OLD DSL INTERPRETER
17
18
 
18
19
  Metrics/BlockLength:
19
20
  Exclude:
@@ -104,3 +105,14 @@ RSpec/SpecFilePathSuffix:
104
105
  RSpec/NamedSubject:
105
106
  Exclude:
106
107
  - "**/spec/**/*"
108
+
109
+ RSpec/MultipleExpectations:
110
+ Exclude:
111
+ - "**/spec/**/*"
112
+
113
+ RSpec/MultipleMemoizedHelpers:
114
+ Exclude:
115
+ - "**/spec/**/*"
116
+
117
+ RSpec/ExampleLength:
118
+ Max: 20
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.1.1](https://github.com/appydave/klue-langcraft/compare/v0.1.0...v0.1.1) (2024-09-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add chatgpt conversation link ([ae5448d](https://github.com/appydave/klue-langcraft/commit/ae5448d4e23ab1fd673c788e6f30e736d82afb44))
7
+
1
8
  # [0.1.0](https://github.com/appydave/klue-langcraft/compare/v0.0.7...v0.1.0) (2024-09-22)
2
9
 
3
10
 
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
5
+
6
+ require 'klue/langcraft'
7
+
8
+ BASE_PATH = ARGV[0] || '/Users/davidcruwys/dev/appydave/klueless'
9
+
10
+ DSLFolderWatcher.watch(BASE_PATH)
@@ -0,0 +1,97 @@
1
+
2
+ # DSL Class Diagram
3
+
4
+ ```plaintext
5
+ +----------------------------+
6
+ | Klue::Langcraft::DSL |
7
+ |----------------------------|
8
+ | + Interpreter |
9
+ | + ProcessMatcher |
10
+ | + ProcessorConfig |
11
+ | + ProcessDataPipeline |
12
+ | + Processors |
13
+ +----------------------------+
14
+
15
+ |
16
+ v
17
+ +-----------------------------------+
18
+ | Interpreter |
19
+ |-----------------------------------|
20
+ | - data: Hash |
21
+ | - processed: Boolean |
22
+ |-----------------------------------|
23
+ | + initialize() |
24
+ | + process(input, output) |
25
+ | + method_missing() |
26
+ | + process_args(args, block) |
27
+ | + respond_to_missing?() |
28
+ | + to_json() |
29
+ +-----------------------------------+
30
+
31
+ |
32
+ v
33
+ +-----------------------------------+
34
+ | ProcessMatcher |
35
+ |-----------------------------------|
36
+ | + match_processors(nodes) |
37
+ |-----------------------------------|
38
+ | - traverse_nodes(node, &block) |
39
+ | - find_processor_for(key, value) |
40
+ |-----------------------------------|
41
+ | - processor_config: ProcessorConfig|
42
+ +-----------------------------------+
43
+
44
+ |
45
+ v
46
+ +-----------------------------------+
47
+ | ProcessDataPipeline |
48
+ |-----------------------------------|
49
+ | + execute(data) |
50
+ | + write_output(data, output_file) |
51
+ |-----------------------------------|
52
+ | - store_result(data, processor, |
53
+ | processed_data) |
54
+ | - calculate_index(data, processor)|
55
+ |-----------------------------------|
56
+ | - matcher: ProcessMatcher |
57
+ +-----------------------------------+
58
+
59
+ |
60
+ v
61
+ +-----------------------------------+
62
+ | ProcessorConfig |
63
+ |-----------------------------------|
64
+ | + register_processor(processor) |
65
+ | + processor_for(key) |
66
+ | + all_processors() |
67
+ |-----------------------------------|
68
+ | - processors: Hash |
69
+ +-----------------------------------+
70
+
71
+ |
72
+ v
73
+ +----------------------------+
74
+ | Processor |
75
+ |----------------------------|
76
+ | - data: Hash |
77
+ | - key: Symbol |
78
+ |----------------------------|
79
+ | + initialize(data, key) |
80
+ | + build_result() |
81
+ | + build_result_data() |
82
+ |----------------------------|
83
+ | + keys() (abstract method) |
84
+ +----------------------------+
85
+
86
+ |
87
+ v
88
+ +----------------------------+ +-------------------------+
89
+ | FileCollectorProcessor | | FullNameProcessor |
90
+ |----------------------------| +-------------------------+
91
+ | (inherits Processor) | | (inherits Processor) |
92
+ |----------------------------| +-------------------------+
93
+ | + build_result_data() | | + build_result_data() |
94
+ | + Auto-register with | | + Auto-register with |
95
+ | ProcessorConfig | | ProcessorConfig |
96
+ +----------------------------+ +-------------------------+
97
+ ```
data/docs/dsl-examples.md CHANGED
@@ -103,3 +103,12 @@ definition :workflow do
103
103
  end
104
104
  end
105
105
  ```
106
+
107
+
108
+ You're going to help me process a Ruby inspired DSL into JSON.
109
+
110
+ There will be two parts of this conversation, part one will be about exploring the concept with a simple reflection based system that will work with any Ruby compatible structure.
111
+
112
+ Part two, which I kick off after I finish part one will be to use an AST style of approach so that I can process the same concept, JavaScript or RUST.
113
+
114
+ To get started, I'm going to give you some sample Ruby DSL and you're going to convert them to an equivalent ruby hash
@@ -0,0 +1,266 @@
1
+ # DSL System Expansion Plan
2
+
3
+ ## Overview
4
+
5
+ The current system processes DSL input using an interpreter, matchers, processors, and pipelines. We need to expand the system by introducing several new features:
6
+
7
+ 1. **Processor Service**: A centralized service that orchestrates all operations, simplifying how the watcher and command-line tools work.
8
+ 2. **Data Saving Option**: An option to toggle whether the processed JSON data is saved to a file or not.
9
+ 3. **Endpoint for Data Delivery**: Add the capability to send the processed data to external HTTP endpoints, configurable via the command line.
10
+
11
+ The changes will affect the class structure, as well as introduce new command-line options and configurations. Below is a detailed explanation of the required features and how they will be implemented.
12
+
13
+ ---
14
+
15
+ ## New Components and Changes
16
+
17
+ ### 1. **Processor Service**
18
+
19
+ - **Purpose**: We need to introduce a new service (tentatively called `ProcessorService`) to orchestrate the different components. This class will:
20
+ - Call the interpreter to process the DSL input.
21
+ - Run the data through the processing pipeline to apply any necessary transformations.
22
+ - Handle the saving of the processed data to a file if configured.
23
+ - Send the resulting JSON data to external endpoints if URLs are provided via the command line.
24
+
25
+ - **Why**: Currently, the watcher or command-line tool is responsible for calling various components (interpreter, pipeline, etc.). By introducing this service, we centralize the logic and simplify the external interfaces. The watcher will only need to call this service, and any configuration options (like file saving or sending data to an endpoint) will be handled internally by the service.
26
+
27
+ - **Key Features**:
28
+ - **Options Handling**: The service will take in various options, such as:
29
+ - Whether to save the processed JSON to a file.
30
+ - URLs for sending data (e.g., one for simplified JSON, one for enhanced JSON).
31
+ - **Sequential Processing**: The service will sequentially call:
32
+ 1. The interpreter to process the DSL.
33
+ 2. The processing pipeline to enhance the data.
34
+ 3. Save the data to a file (if the option is enabled).
35
+ 4. Send the data to the endpoint (if URLs are provided).
36
+
37
+ ### 2. **Data Saving Option**
38
+
39
+ - **Purpose**: Introduce a command-line option (`--save-to-file`) that will enable or disable saving the processed data to a file. By default, the data should flow through the system, but whether it gets written to the filesystem should be configurable.
40
+
41
+ - **Why**: Not all users may want to persist the JSON data to disk. The ability to toggle this behavior will make the system more flexible.
42
+
43
+ - **Details**:
44
+ - When the `--save-to-file` option is enabled, the processed data should be saved to a file.
45
+ - If the option is not provided, the data will not be saved to a file, but it will still flow through the system for other uses (e.g., sending to endpoints).
46
+
47
+ ### 3. **Data Delivery to Endpoints**
48
+
49
+ - **Purpose**: Add the ability to send the JSON data (either simplified or enhanced) to external HTTP endpoints. These endpoints should be configurable via command-line options (`--endpoint-simplified` and `--endpoint-enhanced`).
50
+
51
+ - **Why**: The system should be able to integrate with external services by sending the resulting data to a web application. For example, the web app could receive simplified JSON for debugging and enhanced JSON for final usage. This allows seamless data transfer between the DSL processing system and external applications.
52
+
53
+ - **Details**:
54
+ - The `ProcessorService` will check if any URLs are provided via the command line.
55
+ - If the URLs are provided, it will send the resulting JSON data (using HTTP POST) to those endpoints.
56
+ - **Two endpoints**:
57
+ - One for simplified JSON.
58
+ - One for enhanced JSON.
59
+ - The system should allow sending data to either or both endpoints, depending on the provided options.
60
+
61
+ ### 4. **Command-Line Interface (Expanded)**
62
+
63
+ - The existing `CommandLineInterface` class should be expanded to handle the new options:
64
+ - `--save-to-file`: Toggles whether the JSON is saved to a file.
65
+ - `--endpoint-simplified`: Specifies the URL for sending simplified JSON.
66
+ - `--endpoint-enhanced`: Specifies the URL for sending enhanced JSON.
67
+
68
+ - These options should be passed to the `ProcessorService`, which will handle the actual behavior (e.g., saving the file, sending data to the endpoint).
69
+
70
+ ### 5. **Watcher Integration**
71
+
72
+ - The existing `Watcher` class, which uses `ListenGem` to monitor file changes, will now delegate all processing to the `ProcessorService`. When a file change is detected, the watcher should simply call the service with the necessary options.
73
+
74
+ - This means that the watcher doesn't need to know about interpreters, pipelines, or processors—it just needs to hand over the file to the `ProcessorService`, which will handle everything sequentially.
75
+
76
+ ---
77
+
78
+ ## Revised Class Structure
79
+
80
+ ### New Classes:
81
+
82
+ 1. **ProcessorService**:
83
+ - Central orchestrator for calling the interpreter, processing data, saving the file, and sending data to endpoints.
84
+ - Will take all options from the command line and watcher.
85
+
86
+ 2. **DataDeliveryService**:
87
+ - Handles sending JSON data to external HTTP endpoints.
88
+ - Accepts a URL and sends the provided data using HTTP POST.
89
+
90
+ ### Expanded Classes:
91
+
92
+ 1. **CommandLineInterface**:
93
+ - Now handles additional command-line options (`--save-to-file`, `--endpoint-simplified`, `--endpoint-enhanced`).
94
+ - Passes these options to the `ProcessorService`.
95
+
96
+ 2. **Watcher**:
97
+ - Instead of directly calling interpreters and pipelines, the watcher will now pass the detected file changes to the `ProcessorService` for handling.
98
+
99
+ ---
100
+
101
+ ## Command-Line Options
102
+
103
+ - **New Options**:
104
+ - `--save-to-file`: If provided, the resulting JSON will be saved to a file. Otherwise, the data will only flow through the system.
105
+ - `--endpoint-simplified`: A URL for sending the simplified JSON to an external endpoint.
106
+ - `--endpoint-enhanced`: A URL for sending the enhanced JSON to an external endpoint.
107
+
108
+ These options should be passed to the `ProcessorService`, which will then handle the appropriate behavior based on the configuration.
109
+
110
+ ---
111
+
112
+ ## Final Workflow
113
+
114
+ 1. **Command-Line Usage**:
115
+ - Users can pass options such as directories to watch, whether to save files, and URLs for endpoints.
116
+
117
+ 2. **Watcher**:
118
+ - Monitors the specified directories for file changes and passes any detected changes to the `ProcessorService`.
119
+
120
+ 3. **ProcessorService**:
121
+ - Orchestrates the sequence:
122
+ 1. Calls the `Interpreter` to process the DSL input.
123
+ 2. Runs the `ProcessDataPipeline` to enhance the data.
124
+ 3. Saves the resulting data to a file if the `--save-to-file` option is enabled.
125
+ 4. Sends the resulting data to the provided URL(s) if the `--endpoint-simplified` or `--endpoint-enhanced` options are specified.
126
+
127
+ 4. **Data Delivery**:
128
+ - The `DataDeliveryService` is responsible for sending the processed data to the external endpoints, handling any HTTP interactions required.
129
+
130
+
131
+ # SAMPLE CODE, NOT TESTED BUT MIGHT BE SUITABLE
132
+ ```ruby
133
+
134
+ # file: lib/klue/langcraft/dsl/data_delivery_service.rb
135
+ require 'net/http'
136
+ require 'uri'
137
+ require 'json'
138
+
139
+ module Klue
140
+ module Langcraft
141
+ module DSL
142
+ class DataDeliveryService
143
+ def initialize(url)
144
+ @url = URI.parse(url)
145
+ end
146
+
147
+ def send(data)
148
+ http = Net::HTTP.new(@url.host, @url.port)
149
+ request = Net::HTTP::Post.new(@url.request_uri, { 'Content-Type' => 'application/json' })
150
+ request.body = data.to_json
151
+ response = http.request(request)
152
+
153
+ puts "Data sent to #{@url}: #{response.code} #{response.message}"
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ ```
160
+
161
+ ```ruby
162
+ # file: lib/klue/langcraft/dsl/processor_service.rb
163
+ require_relative 'interpreter'
164
+ require_relative 'process_data_pipeline'
165
+ require_relative 'data_delivery_service'
166
+
167
+ module Klue
168
+ module Langcraft
169
+ module DSL
170
+ class ProcessorService
171
+ def initialize(options)
172
+ @options = options
173
+ @interpreter = Interpreter.new
174
+ @pipeline = ProcessDataPipeline.new(ProcessMatcher.new)
175
+ end
176
+
177
+ def run
178
+ data = call_interpreter
179
+ enhanced_data = run_pipeline(data)
180
+ save_to_file(enhanced_data) if @options[:save_to_file]
181
+ send_to_endpoint(enhanced_data)
182
+ end
183
+
184
+ private
185
+
186
+ def call_interpreter
187
+ @interpreter.process(input: 'path_to_input_file.dsl')
188
+ end
189
+
190
+ def run_pipeline(data)
191
+ @pipeline.execute(data)
192
+ end
193
+
194
+ def save_to_file(data)
195
+ File.write('output.json', JSON.pretty_generate(data))
196
+ end
197
+
198
+ def send_to_endpoint(data)
199
+ if @options[:endpoint_simplified]
200
+ DataDeliveryService.new(@options[:endpoint_simplified]).send(data)
201
+ end
202
+
203
+ if @options[:endpoint_enhanced]
204
+ DataDeliveryService.new(@options[:endpoint_enhanced]).send(data)
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ ```
213
+
214
+ ```ruby
215
+ # file: lib/klue/langcraft/dsl/command_line_interface.rb
216
+ require_relative 'watcher'
217
+ require_relative 'processor_service'
218
+
219
+ module Klue
220
+ module Langcraft
221
+ module DSL
222
+ class CommandLineInterface
223
+ def initialize
224
+ @watch_dirs = []
225
+ @options = {}
226
+ end
227
+
228
+ def start
229
+ parse_arguments
230
+ start_watcher_or_processor_service
231
+ end
232
+
233
+ private
234
+
235
+ def parse_arguments
236
+ ARGV.each_with_index do |arg, index|
237
+ case arg
238
+ when '--watch'
239
+ @watch_dirs << ARGV[index + 1]
240
+ when '--save-to-file'
241
+ @options[:save_to_file] = true
242
+ when '--endpoint-simplified'
243
+ @options[:endpoint_simplified] = ARGV[index + 1]
244
+ when '--endpoint-enhanced'
245
+ @options[:endpoint_enhanced] = ARGV[index + 1]
246
+ end
247
+ end
248
+
249
+ raise 'No directories specified for watching' if @watch_dirs.empty?
250
+ end
251
+
252
+ def start_watcher_or_processor_service
253
+ if @watch_dirs.any?
254
+ watcher = Watcher.new(@watch_dirs)
255
+ watcher.start
256
+ else
257
+ processor_service = ProcessorService.new(@options)
258
+ processor_service.run
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ ```
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # file: lib/base_process.rb
4
+
5
+ class BaseProcess
6
+ attr_reader :key
7
+
8
+ def initialize(key)
9
+ @key = key
10
+ end
11
+
12
+ def deep_match(input, predicate)
13
+ matches = []
14
+
15
+ # If the current input is a Hash, iterate over each key-value pair
16
+ if input.is_a?(Hash)
17
+
18
+ input.each do |key, value|
19
+
20
+ # If the value matches the predicate, add it to matches
21
+ if predicate.call(key, value)
22
+ matches << value
23
+ end
24
+
25
+ # Continue searching deeper within the value
26
+ matches.concat(deep_match(value, predicate))
27
+ end
28
+
29
+ # If the input is an Array, iterate over each item
30
+ elsif input.is_a?(Array)
31
+
32
+ input.each do |item|
33
+
34
+ # Continue searching within each item of the array
35
+ matches.concat(deep_match(item, predicate))
36
+ end
37
+ end
38
+
39
+ matches
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DSLFolderWatcher
4
+ def self.watch(folder_path)
5
+ puts "Watching: #{folder_path}"
6
+ listener = Listen.to(folder_path) do |modified, added, _removed|
7
+ changes = (modified + added).uniq
8
+
9
+ # DEBOUNCE CURRENTLY NOT WORKING
10
+ # debounce_map = {}
11
+ # debounce_interval = 1 # seconds
12
+
13
+ changes.each do |file_path|
14
+ next unless File.extname(file_path) == '.klue'
15
+
16
+ puts file_path
17
+
18
+ # debounce_map[file_path] ||= Time.now
19
+ # next unless Time.now - debounce_map[file_path] >= debounce_interval
20
+
21
+ # debounce_map[file_path] = Time.now
22
+
23
+ base_name = file_path.gsub(/\.klue$/, '')
24
+ input_file = "#{base_name}.klue"
25
+ output_file = "#{base_name}.json"
26
+
27
+ interpreter = DSLInterpreter.new
28
+ if interpreter.process('', input_file, output_file)
29
+ # Process the JSON data to add 'process-data' details
30
+ dsl_processor = DSLProcessData.new
31
+ dsl_processor.process('', output_file, output_file)
32
+ # SKIP EXTEND FILE FOR NOW AND REWRITE THE OUTPUTFILE
33
+ # dsl_processor.process('', output_file, extended_output_file)
34
+
35
+ # interpreter.send_to_endpoint
36
+ else
37
+ puts 'Skipping further processing due to errors in DSL interpretation.'
38
+ end
39
+ end
40
+
41
+ # Remove old entries from debounce_map to prevent memory bloat
42
+ # debounce_map.each_key do |key|
43
+ # debounce_map.delete(key) if Time.now - debounce_map[key] > debounce_interval * 2
44
+ # end
45
+ end
46
+ listener.start
47
+ puts "Wait for changes: #{folder_path}"
48
+ sleep
49
+ end
50
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ChatReferences:
4
+ # - https://chatgpt.com/c/67064770-d524-8002-8344-3091e895d150
5
+ # - https://chatgpt.com/c/6706289c-9b9c-8002-86e3-f9198c1c608a
6
+ # - https://chatgpt.com/c/670dcd34-5dbc-8002-ad7a-d4df54a6a2e0
7
+ #
8
+ class DSLInterpreter
9
+ def initialize
10
+ @data = {}
11
+ end
12
+
13
+ # Capturing top-level DSL methods
14
+ def method_missing(method_name, *args, &block)
15
+ key = method_name
16
+ value = process_args(args, block)
17
+
18
+ # Append key-value to the current context of @data
19
+ if @data[key]
20
+ @data[key] = [@data[key]] unless @data[key].is_a?(Array)
21
+ @data[key] << value
22
+ else
23
+ @data[key] = value
24
+ end
25
+ end
26
+
27
+ # A method to handle parameters and nested blocks
28
+ def process_args(args, block)
29
+ data = {}
30
+
31
+ # Handling positional and named parameters separately
32
+ positional_args = []
33
+ named_args = {}
34
+
35
+ args.each do |arg|
36
+ if arg.is_a?(Hash)
37
+ named_args.merge!(arg)
38
+ else
39
+ positional_args << arg
40
+ end
41
+ end
42
+
43
+ # Assign positional parameters generically
44
+ positional_args.each_with_index do |arg, index|
45
+ data[:"param#{index + 1}"] = arg
46
+ end
47
+
48
+ # Merge named parameters directly
49
+ data.merge!(named_args)
50
+
51
+ # Handling a nested block
52
+ if block
53
+ interpreter = DSLInterpreter.new
54
+ interpreter.instance_eval(&block)
55
+ data.merge!(interpreter.data)
56
+ end
57
+
58
+ data.empty? ? nil : data
59
+ end
60
+
61
+ # To access data after interpreting
62
+ attr_reader :data
63
+
64
+ # Reading file and evaluating as Ruby
65
+ def process(base_path, input_file, output_file)
66
+ file_path = File.join(base_path, input_file)
67
+ content = File.read(file_path)
68
+
69
+ # begin
70
+ instance_eval(content)
71
+ # rescue SyntaxError => e
72
+ # puts "Syntax error in DSL file: #{input_file}"
73
+ # puts "Error message: #{e.message}"
74
+ # puts "Error occurred at line: #{e.backtrace.first}"
75
+ # return false # Indicate that processing failed
76
+ # rescue StandardError => e
77
+ # puts "Error processing DSL file: #{input_file}"
78
+ # puts "Error message: #{e.message}"
79
+ # puts "Error occurred at: #{e.backtrace.first}"
80
+ # return false # Indicate that processing failed
81
+ # end
82
+
83
+ output_path = File.join(base_path, output_file)
84
+ File.write(output_path, JSON.pretty_generate(to_hash))
85
+ true # Indicate that processing succeeded
86
+ end
87
+
88
+ # Convert to hash or JSON as required
89
+ def to_hash
90
+ @data
91
+ end
92
+
93
+ def to_json(*_args)
94
+ @data.to_json
95
+ end
96
+
97
+ # Method to send data to an endpoint
98
+ def send_to_endpoint
99
+ root_key = @data.keys.first
100
+ action_type = root_key.to_s
101
+
102
+ uri = URI.parse("http://localhost:4567/dsl/#{action_type}")
103
+ http = Net::HTTP.new(uri.host, uri.port)
104
+ request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
105
+ payload = { action_type: action_type, data: @data }
106
+ request.body = payload.to_json
107
+
108
+ response = http.request(request)
109
+ puts "Response: #{response.code} - #{response.message}"
110
+ puts "Endpoint: #{uri}"
111
+ end
112
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DSLProcessData
4
+ PROCESSORS = [{ file_collector: ProcessFileCollector }].freeze
5
+
6
+ # Method to process the JSON file after initial evaluation
7
+ def process(base_path, input_file, output_file)
8
+ json_file_path = File.join(base_path, input_file)
9
+ data = JSON.parse(File.read(json_file_path))
10
+
11
+ # Loop through the processors and execute matching ones
12
+ PROCESSORS.each do |processor_entry|
13
+ key, processor_class = processor_entry.first
14
+ processor = processor_class.new(key)
15
+
16
+ next unless processor.match?(data)
17
+
18
+ result = processor.execute(data)
19
+
20
+ data['process-data'] ||= {}
21
+
22
+ result.each do |key, result|
23
+ data['process-data'][key.to_s] = result unless result.empty?
24
+ end
25
+ end
26
+
27
+ # Write the updated JSON data to an extended file
28
+ extended_output_file = File.join(base_path, output_file)
29
+ File.write(extended_output_file, JSON.pretty_generate(data))
30
+ end
31
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ChatReferences:
4
+ # - https://chatgpt.com/c/6719840b-c72c-8002-bbc2-bbd95fd98d31
5
+
6
+ module Klue
7
+ module Langcraft
8
+ module DSL
9
+ # Interpreter class for processing and interpreting DSL input
10
+ #
11
+ # This class is responsible for handling method calls, processing arguments,
12
+ # and managing the overall interpretation of the DSL input. It also provides
13
+ # methods for processing input and output, as well as converting the data
14
+ # to hash and JSON formats.
15
+ class Interpreter
16
+ attr_reader :data
17
+ attr_accessor :processed
18
+
19
+ def initialize
20
+ @data = {}
21
+ @processed = false
22
+ end
23
+
24
+ def process(input: nil, input_file: nil, output_file: nil)
25
+ validate_input_arguments(input, input_file)
26
+ input_content = input_content(input, input_file)
27
+
28
+ @processed = true
29
+ instance_eval(input_content)
30
+
31
+ write_output(output_file) if output_file
32
+ data
33
+ end
34
+
35
+ def method_missing(method_name, *args, &block)
36
+ raise "You must call 'process' before using other methods" unless @processed
37
+
38
+ key = method_name
39
+ value = process_args(args, block)
40
+
41
+ if @data[key]
42
+ @data[key] = [@data[key]] unless @data[key].is_a?(Array)
43
+ @data[key] << value
44
+ else
45
+ @data[key] = value
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(method_name, include_private = false)
50
+ @processed || super
51
+ end
52
+
53
+ def process_args(args, block)
54
+ positional_args = []
55
+ named_args = {}
56
+
57
+ # Handling positional and named parameters separately
58
+ args.each do |arg|
59
+ if arg.is_a?(Hash)
60
+ named_args.merge!(arg)
61
+ else
62
+ positional_args << arg
63
+ end
64
+ end
65
+
66
+ # Assign positional parameters generically
67
+ data = positional_args.each_with_index.to_h { |arg, index| [:"p#{index + 1}", arg] }
68
+
69
+ # Merge named parameters after positional ones
70
+ data.merge!(named_args)
71
+
72
+ # Handling a nested block
73
+ if block
74
+ interpreter = Interpreter.new
75
+ interpreter.instance_variable_set(:@processed, true) # Set @processed to true for nested interpreter
76
+ interpreter.instance_eval(&block)
77
+ data.merge!(interpreter.data)
78
+ end
79
+
80
+ data.empty? ? nil : data
81
+ end
82
+
83
+ private
84
+
85
+ def validate_input_arguments(input, input_file)
86
+ raise ArgumentError, 'Either input or input_file must be provided' unless input || input_file
87
+ raise ArgumentError, 'Both input and input_file cannot be provided' if input && input_file
88
+ end
89
+
90
+ def input_content(input, input_file)
91
+ input_file ? File.read(input_file) : input
92
+ end
93
+
94
+ def write_output(output_file)
95
+ output_path = get_output_path(output_file)
96
+ File.write(output_path, JSON.pretty_generate(data))
97
+ end
98
+
99
+ def get_output_path(output_file)
100
+ if Pathname.new(output_file).absolute?
101
+ output_file
102
+ else
103
+ File.join(File.dirname(output_file), File.basename(output_file))
104
+ end
105
+ end
106
+
107
+ # Convert to JSON
108
+ def to_json(*_args)
109
+ @data.to_json
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ # ProcessDataPipeline class for executing data processing pipelines
7
+ #
8
+ # This class is responsible for executing a series of data processing steps
9
+ # based on the configured processors. It manages the flow of data through
10
+ # the pipeline and handles the storage of results.
11
+ class ProcessDataPipeline
12
+ def initialize(matcher)
13
+ @matcher = matcher # Use the matcher to find processors
14
+ end
15
+
16
+ # Execute the pipeline of processors on the input data
17
+ def execute(data)
18
+ # NOTE: This is the complete data object, each processor will get a cloned version the specific data it is matched to
19
+ matched_processors = @matcher.match_processors(data)
20
+
21
+ matched_processors.each do |processor|
22
+ processed_data = processor.build_result
23
+
24
+ # Store the processed data into the result structure
25
+ store_result(data, processor, processed_data)
26
+ end
27
+
28
+ data
29
+ end
30
+
31
+ # Optionally write the output to a file
32
+ def write_output(data, output_file)
33
+ File.write(output_file, JSON.pretty_generate(data))
34
+ end
35
+
36
+ private
37
+
38
+ # Store the processed result back into the data structure
39
+ def store_result(data, _processor, processed_data)
40
+ return unless processed_data
41
+
42
+ data['process-data'] ||= {}
43
+
44
+ if processed_data[:name].nil? || processed_data[:name].empty?
45
+ index = calculate_index(data, processed_data[:type])
46
+ processed_data[:name] = "#{processed_data[:type]}-#{index}"
47
+ end
48
+
49
+ data['process-data'][processed_data[:name]] = processed_data
50
+ end
51
+
52
+ def calculate_index(data, processor_type)
53
+ # Find all keys in 'process-data' that match the processor type (e.g., file_collector)
54
+ last_index = data['process-data'].keys
55
+ .select { |k| k.start_with?(processor_type.to_s) } # Keys that start with processor type
56
+ .map { |k| k.split('-').last.to_i } # Extract numeric suffix
57
+ .max
58
+
59
+ # If no entries exist, start at 1; otherwise, increment the last index
60
+ last_index ? last_index + 1 : 1
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ # ProcessMatcher class for matching processors to input nodes
7
+ #
8
+ # This class is responsible for traversing input nodes and finding
9
+ # the appropriate processor for each node based on the configured
10
+ # processor rules.
11
+ class ProcessMatcher
12
+ def initialize(processor_config = ProcessorConfigDefault)
13
+ @processor_config = processor_config
14
+ end
15
+
16
+ def match_processors(nodes)
17
+ matched_processors = []
18
+
19
+ traverse_nodes(nodes) do |key, value|
20
+ processor_class = find_processor_for(key, value)
21
+ if processor_class
22
+ if value.is_a?(Array)
23
+ # If the value is an array, instantiate a processor for each element
24
+ value.each do |element|
25
+ matched_processors << processor_class.new(element, key)
26
+ end
27
+ else
28
+ matched_processors << processor_class.new(value, key)
29
+ end
30
+ end
31
+ end
32
+
33
+ matched_processors
34
+ end
35
+
36
+ private
37
+
38
+ def traverse_nodes(node, &block)
39
+ if node.is_a?(Hash)
40
+ node.each do |key, value|
41
+ yield(key, value)
42
+ traverse_nodes(value, &block)
43
+ end
44
+ elsif node.is_a?(Array)
45
+ node.each_with_index do |item, index|
46
+ # Provide the index to uniquely identify each element
47
+ traverse_nodes(item) { |key, value| yield("#{key}[#{index}]", value) }
48
+ end
49
+ end
50
+ end
51
+
52
+ # Find the correct processor based on the key using the registered processor config
53
+ def find_processor_for(key, _value)
54
+ @processor_config.processor_for(key) # Return the processor class, not an instance
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ # ProcessorConfig class for managing processor configurations
7
+ #
8
+ # This class is responsible for registering processors and providing
9
+ # methods to retrieve processors based on keys or to get all registered
10
+ # processors.
11
+ class ProcessorConfig
12
+ def initialize
13
+ @processors = {}
14
+ end
15
+
16
+ # Register a processor with its associated keys
17
+ def register_processor(processor_class)
18
+ keys = processor_class.keys
19
+ keys = [keys] unless keys.is_a?(Array)
20
+ keys.each { |key| @processors[key.to_sym] = processor_class }
21
+ end
22
+
23
+ # Find the processor class by key
24
+ def processor_for(key)
25
+ @processors[key.to_sym]
26
+ end
27
+
28
+ # List all registered processors
29
+ def all_processors
30
+ @processors
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ module Processors
7
+ # FileCollectorProcessor class for processing file-related data
8
+ #
9
+ # This processor is responsible for handling file collection operations
10
+ # within the DSL. It inherits from the base Processor class and implements
11
+ # specific logic for file-related processing.
12
+ class FileCollectorProcessor < Processor
13
+ def self.keys
14
+ [:file_collector]
15
+ end
16
+
17
+ # Example of how a subclass can implement its specific data logic
18
+ def build_result_data
19
+ {
20
+ files: ['file1.txt', 'file2.txt']
21
+ }
22
+ end
23
+
24
+ # Auto-register the processor as soon as the class is loaded
25
+ ProcessorConfigDefault.register_processor(self)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ module Processors
7
+ # FullNameProcessor class for processing full name data
8
+ #
9
+ # This processor is responsible for handling full name processing operations
10
+ # within the DSL. It inherits from the base Processor class and implements
11
+ # specific logic for full name-related processing.
12
+ class FullNameProcessor < Processor
13
+ def self.keys
14
+ [:full_name] # FullNameProcessor's specific key(s)
15
+ end
16
+
17
+ # Implementation of logic for building full name data
18
+ def build_result_data
19
+ first_name = data['first_name'] || 'John'
20
+ last_name = data['last_name'] || 'Doe'
21
+ full_name = "#{first_name} #{last_name}"
22
+
23
+ {
24
+ full_name: full_name
25
+ }
26
+ end
27
+
28
+ # Auto-register the processor as soon as the class is loaded
29
+ ProcessorConfigDefault.register_processor(self)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klue
4
+ module Langcraft
5
+ module DSL
6
+ module Processors
7
+ # Base Processor class for defining common processor behavior
8
+ #
9
+ # This abstract class serves as the foundation for all specific processors.
10
+ # It defines the basic structure and common methods that all processors
11
+ # should implement or inherit.
12
+ class Processor
13
+ attr_reader :data, :key
14
+
15
+ # Every processor subclass must accept data and key
16
+ def initialize(data, key)
17
+ @data = Marshal.load(Marshal.dump(data)) # Deep clone of the data
18
+ @key = key
19
+ end
20
+
21
+ # Build an envelope result with type, name, and data
22
+ def build_result
23
+ {
24
+ name: data.is_a?(Hash) ? data['as'] : nil,
25
+ type: key.to_s,
26
+ data: build_result_data
27
+ }
28
+ end
29
+
30
+ # Subclasses should override this method to build the actual data.
31
+ def build_result_data
32
+ raise NotImplementedError, 'Subclasses must implement `build_data` to generate their specific data'
33
+ end
34
+
35
+ # This will be overridden by subclasses to define keys (or aliases)
36
+ def self.keys
37
+ raise NotImplementedError, 'Subclasses must define the `keys` method'
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Klue
4
4
  module Langcraft
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -1,6 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'listen'
7
+ require 'pry'
8
+
9
+ require 'appydave/tools'
10
+
3
11
  require 'klue/langcraft/version'
12
+ require 'klue/langcraft/dsl/processor_config'
13
+
14
+ ProcessorConfigDefault = Klue::Langcraft::DSL::ProcessorConfig.new
15
+
16
+ require 'klue/langcraft/dsl/processors/processor'
17
+ require 'klue/langcraft/dsl/processors/file_collector_processor'
18
+ require 'klue/langcraft/dsl/processors/full_name_processor'
19
+ require 'klue/langcraft/dsl/interpreter'
20
+ require 'klue/langcraft/dsl/process_matcher'
21
+ require 'klue/langcraft/dsl/process_data_pipeline'
22
+
23
+ require 'base_process'
24
+ require 'process_file_collector'
25
+ require 'dsl_interpreter'
26
+ require 'dsl_folder_watcher'
27
+ require 'dsl_process_data'
4
28
 
5
29
  module Klue
6
30
  module Langcraft
@@ -12,8 +36,8 @@ module Klue
12
36
  end
13
37
 
14
38
  if ENV.fetch('KLUE_DEBUG', 'false').downcase == 'true'
15
- namespace = 'KlueLangcraft::Version'
39
+ namespace = 'Klue::Langcraft::Version'
16
40
  file_path = $LOADED_FEATURES.find { |f| f.include?('klue-langcraft/version') }
17
- version = KlueLangcraft::VERSION.ljust(9)
41
+ version = Klue::Langcraft::VERSION.ljust(9)
18
42
  puts "#{namespace.ljust(35)} : #{version.ljust(9)} : #{file_path}"
19
43
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # file: lib/process_file_collector.rb
4
+
5
+ class ProcessFileCollector < BaseProcess
6
+ def initialize(key)
7
+ super
8
+ @found = nil
9
+ # @matcher = ->(key, value) { key.to_s == 'file_collector' && value.is_a?(Hash) }
10
+ @matcher = lambda do |key, value|
11
+ if key.to_s == 'file_collector'
12
+ if value.is_a?(Array)
13
+ value.any? { |v| v.is_a?(Hash) }
14
+ else
15
+ value.is_a?(Hash)
16
+ end
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ def match?(input)
24
+ @found = deep_match(input, @matcher)
25
+ !@found.empty?
26
+ end
27
+
28
+ def execute(_input)
29
+ # Iterate over each `file_collector` found and process individually
30
+ results = {}
31
+
32
+ @found.each do |data|
33
+ next unless data.is_a?(Hash)
34
+
35
+ # Extract the `as` key if present
36
+ as_key = data['as']
37
+
38
+ working_directory = File.expand_path(data['root'])
39
+
40
+ options = Appydave::Tools::GptContext::Options.new(
41
+ working_directory: working_directory,
42
+ include_patterns: extract_patterns(data.dig('files', 'include')),
43
+ exclude_patterns: extract_patterns(data.dig('files', 'exclude')),
44
+ format: 'json',
45
+ line_limit: data['line_length']
46
+ )
47
+
48
+ collector = Appydave::Tools::GptContext::FileCollector.new(options)
49
+ json = collector.build
50
+
51
+ # Structuring the result under `process-data` with `as` as key
52
+ result_data = {
53
+ type: 'file_collector',
54
+ data: {
55
+ working_directory: working_directory,
56
+ files: JSON.parse(json)
57
+ }
58
+ }
59
+
60
+ # If `as` key exists, use it to store under process-data with that identifier
61
+ if as_key
62
+ results[as_key] = result_data
63
+ else
64
+ # Generate a unique key if no `as` key is defined
65
+ unique_key = "file_collector_#{results.size + 1}"
66
+ results[unique_key] = result_data
67
+ end
68
+ end
69
+
70
+ results
71
+ rescue SyntaxError, NameError, NoMethodError => e
72
+ puts "Ruby evaluation error in ProcessFileCollector: #{e.message}"
73
+ puts "Error occurred at: #{e.backtrace.first}"
74
+ {}
75
+ rescue StandardError => e
76
+ puts "Unexpected error in ProcessFileCollector: #{e.message}"
77
+ puts e.backtrace.join("\n")
78
+ {}
79
+ end
80
+
81
+ private
82
+
83
+ def extract_patterns(files_data)
84
+ if files_data.is_a?(Hash)
85
+ [files_data['param1']]
86
+ elsif files_data.is_a?(Array)
87
+ files_data.map { |entry| entry['param1'] }
88
+ else
89
+ []
90
+ end
91
+ end
92
+ end
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "klue-langcraft",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "klue-langcraft",
9
- "version": "0.1.1",
9
+ "version": "0.2.0",
10
10
  "devDependencies": {
11
11
  "@klueless-js/semantic-release-rubygem": "github:klueless-js/semantic-release-rubygem",
12
12
  "@semantic-release/changelog": "^6.0.3",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klue-langcraft",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Domain Specific Language Crafting",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: klue-langcraft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-22 00:00:00.000000000 Z
11
+ date: 2024-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: appydave-tools
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: k_log
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -47,27 +61,41 @@ files:
47
61
  - README.md
48
62
  - Rakefile
49
63
  - bin/console
64
+ - bin/dsl_watcher.rb
50
65
  - bin/setup
66
+ - docs/dsl-class-diagram.md
51
67
  - docs/dsl-examples.md
52
68
  - docs/dsl-rules.md
53
- - docs/dsl-samples/index.md
54
69
  - docs/dsl-samples/youtube-launch-optimizer-old.klue
55
70
  - docs/dsl-samples/youtube-launch-optimizer-strawberry.json
56
71
  - docs/dsl-samples/youtube-launch-optimizer-strawberry.klue
57
72
  - docs/dsl-samples/youtube-launch-optimizer.defn.klue
58
73
  - docs/dsl-samples/youtube-launch-optimizer.json
59
74
  - docs/dsl-samples/youtube-launch-optimizer.klue
75
+ - docs/dsl-upgrade-plan.md
60
76
  - docs/project-plan/project-plan.md
61
77
  - docs/project-plan/project.drawio
62
78
  - docs/project-plan/project_done.svg
63
79
  - docs/project-plan/project_in_progress.svg
64
80
  - docs/project-plan/project_todo.svg
81
+ - lib/base_process.rb
82
+ - lib/dsl_folder_watcher.rb
83
+ - lib/dsl_interpreter.rb
84
+ - lib/dsl_process_data.rb
65
85
  - lib/klue/langcraft.rb
66
- - lib/klue/langcraft/-brief.md
67
- - lib/klue/langcraft/parser.rb
68
- - lib/klue/langcraft/sample_usage.rb
69
- - lib/klue/langcraft/tokenizer.rb
86
+ - lib/klue/langcraft/dsl/interpreter.rb
87
+ - lib/klue/langcraft/dsl/process_data_pipeline.rb
88
+ - lib/klue/langcraft/dsl/process_matcher.rb
89
+ - lib/klue/langcraft/dsl/processor_config.rb
90
+ - lib/klue/langcraft/dsl/processors/file_collector_processor.rb
91
+ - lib/klue/langcraft/dsl/processors/full_name_processor.rb
92
+ - lib/klue/langcraft/dsl/processors/processor.rb
93
+ - lib/klue/langcraft/tokenizer-old-needs-revisit/-brief.md
94
+ - lib/klue/langcraft/tokenizer-old-needs-revisit/parser.rb
95
+ - lib/klue/langcraft/tokenizer-old-needs-revisit/sample_usage.rb
96
+ - lib/klue/langcraft/tokenizer-old-needs-revisit/tokenizer.rb
70
97
  - lib/klue/langcraft/version.rb
98
+ - lib/process_file_collector.rb
71
99
  - package-lock.json
72
100
  - package.json
73
101
  - sig/klue/langcraft.rbs
@@ -1,4 +0,0 @@
1
- Printspeak DSL
2
- Project Plan
3
- Agent As Code
4
- Make Chapters out of my recordings using a folder and glob pattern geared to my naming convention