foobara-typescript-react-command-form-generator 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 046d876be59b51bea2745841e8155702344c33560214efb6b2a6b2381a49bc72
4
+ data.tar.gz: '0955731184338275963f896dd867544cbfcc2b0bfe8886b1c54715e9bc5878ed'
5
+ SHA512:
6
+ metadata.gz: 6369d3afc06b6b163aa3320baa2983ff8e17b15bc561aeb2f59262d272a09d0c5af83fe5f436dca88885341586376a76543f4a8267afd1a5423db0f2f4af4aa7
7
+ data.tar.gz: 75e11096949e2439fe1daaee35c09f5670e098893c5b128a6b2ecb06cd6aadbcfd8c7b5fee79d76fdec326271eb3ac40970a693a05eee472514a6d5b2498bfa6
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [0.0.1] - 2024-06-17
2
+
3
+ - Add Apache-2.0 license
4
+
5
+ ## [0.0.0] - 2024-03-13
6
+
7
+ - Project birth
data/LICENSE.txt ADDED
@@ -0,0 +1,8 @@
1
+ This project is dual licensed under your choice of the Apache-2.0 license and the MIT license.
2
+
3
+ Apache-2.0 License: LICENSE-APACHE.txt or https://www.apache.org/licenses/LICENSE-2.0.txt
4
+ MIT License: LICENSE-MIT.txt or https://opensource.org/licenses/MIT
5
+
6
+ This is equivalent to the following SPDX License Expression: Apache-2.0 OR MIT
7
+
8
+ Copyright (c) 2024 Miles Georgi
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Foobara::CommandGenerator
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library
6
+ into a gem. Put your Ruby code in the file `lib/foobara/typescript_react_command_form_generator`. To experiment with
7
+ that code,
8
+ run `bin/console` for an interactive prompt.
9
+
10
+ ## Installation
11
+
12
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it
13
+ to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with
14
+ instructions to install your gem from git if you don't plan to release to RubyGems.org.
15
+
16
+ Install the gem and add to the application's Gemfile by executing:
17
+
18
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
19
+
20
+ If bundler is not being used to manage dependencies, install the gem by executing:
21
+
22
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
23
+
24
+ ## Usage
25
+
26
+ TODO: Write usage instructions here
27
+
28
+ ## Development
29
+
30
+ If using Foobara locally, then run the following (TODO: make this no-longer necessary.)
31
+
32
+ ```bash
33
+ bundle config set local.foobara ../foobara
34
+ bundle config set disable_local_branch_check true
35
+ ```
36
+
37
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
38
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
39
+
40
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
41
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
42
+ push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
43
+
44
+ ## Contributing
45
+
46
+ Bug reports and pull requests are welcome on GitHub
47
+ at https://github.com/[USERNAME]/foobara-typescript-react-command-form-generator.
48
+
49
+ ## License
50
+
51
+ This project is licensed under your choice of the Apache-2.0 license or the MIT license.
52
+ See [LICENSE.txt](LICENSE.txt) for more info about licensing.
@@ -0,0 +1,4 @@
1
+ require "foobara/all"
2
+ require "foobara/typescript_remote_command_generator"
3
+
4
+ Foobara::Util.require_directory "#{__dir__}/../../src"
@@ -0,0 +1,66 @@
1
+ module Foobara
2
+ module Generators
3
+ module TypescriptReactCommandFormGenerator
4
+ class GenerateTypescriptReactCommandForm < Foobara::RemoteGenerator::GenerateTypescript
5
+ class BadCommandNameError < Value::DataError
6
+ class << self
7
+ def context_type_declaration
8
+ {
9
+ bad_name: :string,
10
+ valid_names: [:string]
11
+ }
12
+ end
13
+ end
14
+ end
15
+
16
+ possible_input_error :command_name, BadCommandNameError
17
+
18
+ inputs do
19
+ raw_manifest :associative_array
20
+ manifest_url :string
21
+ command_name :string, :required
22
+ end
23
+
24
+ attr_accessor :manifest_data, :command_manifest
25
+
26
+ def base_generator
27
+ Generators::TypescriptReactCommandFormGenerator
28
+ end
29
+
30
+ def execute
31
+ load_manifest_if_needed
32
+ find_command_manifest
33
+ add_command_manifest_to_set_of_elements_to_generate
34
+
35
+ each_element_to_generate do
36
+ generate_element
37
+ end
38
+
39
+ paths_to_source_code
40
+ end
41
+
42
+ def find_command_manifest
43
+ self.command_manifest = Manifest::Command.new(manifest_data, [:command, command_name])
44
+ rescue Manifest::InvalidPath
45
+ valid_keys = manifest_data["command"].keys.sort
46
+ message = "Invalid command name: #{command_name}. Expected one of #{valid_keys.join(", ")}"
47
+ error = BadCommandNameError.new(
48
+ message:,
49
+ context: {
50
+ bad_name: command_name,
51
+ valid_names: valid_keys
52
+ },
53
+ path: [:command_name]
54
+ )
55
+
56
+ add_input_error(error)
57
+ halt!
58
+ end
59
+
60
+ def add_command_manifest_to_set_of_elements_to_generate
61
+ elements_to_generate << command_manifest
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,229 @@
1
+ module Foobara
2
+ module Generators
3
+ module TypescriptReactCommandFormGenerator
4
+ module Generators
5
+ class TypescriptReactCommandFormGenerator < RemoteGenerator::Services::TypescriptFromManifestBaseGenerator
6
+ class << self
7
+ def manifest_to_generator_classes(manifest)
8
+ case manifest
9
+ when Manifest::Command
10
+ [
11
+ Generators::TypescriptReactCommandFormGenerator
12
+ ]
13
+ else
14
+ # :nocov:
15
+ super
16
+ # :nocov:
17
+ end
18
+ end
19
+ end
20
+
21
+ def template_path
22
+ "CommandForm.tsx.erb"
23
+ end
24
+
25
+ def target_path
26
+ *parts, command_name = scoped_full_path
27
+
28
+ ["forms", *parts, "#{command_name}Form.tsx"]
29
+ end
30
+
31
+ def command_generator
32
+ @command_generator ||= RemoteGenerator::Services::CommandGenerator.new(command_manifest)
33
+ end
34
+
35
+ def model_generators(type = inputs_type, initial = true)
36
+ return @model_generators if defined?(@model_generators)
37
+
38
+ generators = if type.entity?
39
+ generator_class = RemoteGenerator::Services::UnloadedEntityGenerator
40
+ [generator_class.new(type.to_entity)]
41
+ elsif type.model?
42
+ generator_class = RemoteGenerator::Services::AtomModelGenerator
43
+ [generator_class.new(type.to_model)]
44
+ elsif type.type.to_sym == :attributes
45
+ type.attribute_declarations.values.map do |attribute_declaration|
46
+ model_generators(attribute_declaration, false)
47
+ end.flatten.uniq
48
+ elsif type.is_a?(Manifest::Array)
49
+ if type.element_type
50
+ model_generators(type.element_type, false)
51
+ end
52
+ else
53
+ # TODO: handle tuples and associative arrays
54
+ []
55
+ end
56
+
57
+ if initial
58
+ @model_generators = generators
59
+ end
60
+
61
+ generators
62
+ end
63
+
64
+ def dependencies
65
+ [model_generators, *model_generators.map(&:dependencies)].flatten.uniq
66
+ end
67
+
68
+ def dependencies_to_generate
69
+ []
70
+ end
71
+
72
+ alias command_manifest relevant_manifest
73
+
74
+ def templates_dir
75
+ "#{__dir__}/../templates"
76
+ end
77
+
78
+ def inputs_class_name
79
+ "#{command_name}Inputs"
80
+ end
81
+
82
+ def result_class_name
83
+ "#{command_name}Result"
84
+ end
85
+
86
+ def error_class_name
87
+ "#{command_name}Error"
88
+ end
89
+
90
+ def command_name_english
91
+ Util.humanize(Util.underscore(command_name))
92
+ end
93
+
94
+ def non_colliding_inputs(type_declaration = inputs_type, result = [], path = [])
95
+ if type_declaration.attributes?
96
+ type_declaration.attribute_declarations.each_pair do |attribute_name, attribute_declaration|
97
+ non_colliding_inputs(attribute_declaration, result, [*path, attribute_name])
98
+ end
99
+ elsif type_declaration.entity?
100
+ # TODO: figure out how to not pass self here...
101
+ result << FlattenedAttribute.new(self, path, type_declaration.to_type.primary_key_type)
102
+ elsif type_declaration.model?
103
+ non_colliding_inputs(type_declaration.to_type.attributes_type, result, path)
104
+ elsif type_declaration.array?
105
+ if type_declaration.element_type
106
+ model_generators(type_declaration.element_type, false)
107
+ end
108
+ else
109
+ result << FlattenedAttribute.new(self, path, type_declaration)
110
+ end
111
+
112
+ result
113
+ end
114
+
115
+ def populated_inputs_object
116
+ result = {}
117
+
118
+ non_colliding_inputs.each do |flattened_attribute|
119
+ DataPath.set_value_at(result, flattened_attribute.name, flattened_attribute.path)
120
+ rescue DataPath::BadPathError
121
+ value = result
122
+ parts = flattened_attribute.path[..-2]
123
+
124
+ parts.each do |part|
125
+ value = value[part] ||= {}
126
+ end
127
+
128
+ DataPath.set_value_at(result, flattened_attribute.name, flattened_attribute.path)
129
+ end
130
+
131
+ _to_ts_string(result)
132
+ end
133
+
134
+ # TODO: come up with a better name for this and its parameter
135
+ def _to_ts_string(result, depth: 0)
136
+ if result.is_a?(::Hash)
137
+ output = "#{" " * depth}{\n"
138
+ result.each_pair.with_index do |(key, value), index|
139
+ output << "#{" " * depth} #{key}: #{_to_ts_string(value, depth: depth + 2)}"
140
+
141
+ if index != result.size - 1
142
+ output << ","
143
+ end
144
+
145
+ output << "\n"
146
+ end
147
+
148
+ "#{output}#{" " * depth}}\n"
149
+ elsif result.is_a?(::String)
150
+ result
151
+ else
152
+ # :nocov:
153
+ raise "Not sure how to handle #{result}"
154
+ # :nocov:
155
+ end
156
+ end
157
+
158
+ class FlattenedAttribute
159
+ attr_accessor :path, :type_declaration, :generator
160
+
161
+ def initialize(generator, path, type_declaration)
162
+ self.generator = generator
163
+ self.path = path
164
+ self.type_declaration = type_declaration
165
+ end
166
+
167
+ def name
168
+ first, *rest = path
169
+
170
+ first = Util.camelize(first)
171
+ rest = rest.map { |part| Util.camelize(part) }
172
+
173
+ [first, *rest].join
174
+ end
175
+
176
+ def name_upcase
177
+ [name[0].upcase, name[1..]].compact.join
178
+ end
179
+
180
+ def ts_type
181
+ generator.foobara_type_to_ts_type(
182
+ type_declaration,
183
+ dependency_group: generator.dependency_group
184
+ )
185
+ end
186
+
187
+ def has_default?
188
+ type_declaration.attribute? && default
189
+ end
190
+
191
+ def default
192
+ type_declaration.default
193
+ end
194
+
195
+ def ts_default
196
+ generator.value_to_ts_value(default)
197
+ end
198
+
199
+ def name_english
200
+ Util.underscore(name).gsub("_", " ")
201
+ end
202
+
203
+ def html_input
204
+ # TODO: handle boolean, etc
205
+ one_of = type_declaration.one_of
206
+
207
+ if one_of
208
+ ts_type = generator.foobara_type_to_ts_type(type_declaration)
209
+
210
+ "<select
211
+ value={#{name} ?? \"\"}
212
+ onChange={(e) => { set#{name_upcase}(e.target.value as #{ts_type}) }}
213
+ >
214
+ #{one_of.map { |value| "<option value=\"#{value}\">#{value}</option>" }.join}
215
+ </select>"
216
+ else
217
+ "<input
218
+ value={#{name} ?? \"\"}
219
+ onChange={(e) => { set#{name_upcase}(e.target.value) }}
220
+ placeholder=\"#{name_english}\"
221
+ />"
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,86 @@
1
+ require_relative "generate_typescript_react_command_form"
2
+
3
+ module Foobara
4
+ module Generators
5
+ module TypescriptReactCommandFormGenerator
6
+ class WriteTypescriptReactCommandFormToDisk < RemoteGenerator::WriteTypescriptToDisk
7
+ class NoManifestError < Foobara::RuntimeError
8
+ class << self
9
+ def context_type_declaration
10
+ {}
11
+ end
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def generator_key
17
+ "typescript-react-command-form"
18
+ end
19
+ end
20
+
21
+ inputs do
22
+ raw_manifest :associative_array, :allow_nil
23
+ manifest_url :string, :allow_nil
24
+ command_name :string, :required
25
+ output_directory :string
26
+ end
27
+
28
+ depends_on GenerateTypescriptReactCommandForm
29
+
30
+ def execute
31
+ load_manifest_if_needed
32
+ generate_typescript
33
+ write_all_files_to_disk
34
+ run_post_generation_tasks
35
+
36
+ stats
37
+ end
38
+
39
+ def validate
40
+ # We don't want to fail if there is no manifest because we might be able to load it from manifest.json
41
+ end
42
+
43
+ def raw_manifest
44
+ @raw_manifest || inputs[:raw_manifest]
45
+ end
46
+
47
+ def load_manifest_if_needed
48
+ if !raw_manifest && !manifest_url
49
+ manifest_path = "#{output_directory}/manifest.json"
50
+ if File.exist?(manifest_path)
51
+ @raw_manifest = JSON.parse(File.read(manifest_path))
52
+ else
53
+ message = "Because there is no manifest.json to read manifest from, " \
54
+ "you must either provide a manifest_url or raw_manifest."
55
+ error = NoManifestError.new(message:, context: {})
56
+
57
+ add_runtime_error(error)
58
+ end
59
+ end
60
+ end
61
+
62
+ def output_directory
63
+ inputs[:output_directory] || default_output_directory
64
+ end
65
+
66
+ def default_output_directory
67
+ # :nocov:
68
+ "src/domains"
69
+ # :nocov:
70
+ end
71
+
72
+ def generate_typescript
73
+ # TODO: we need a way to allow values to be nil in type declarations
74
+ inputs = raw_manifest ? { raw_manifest: } : { manifest_url: }
75
+ inputs.merge!(command_name:)
76
+
77
+ self.paths_to_source_code = run_subcommand!(GenerateTypescriptReactCommandForm, inputs)
78
+ end
79
+
80
+ def run_post_generation_tasks
81
+ eslint_fix
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,72 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import { Outcome } from "<%= path_to_root %>/base/Outcome"
4
+
5
+ import { <%= command_name %> } from "<%= path_to_root %><%= command_generator.import_path %>"
6
+ import <%= inputs_class_name %> from "<%= path_to_root %><%= command_generator.import_path %>/Inputs"
7
+ import <%= result_class_name %> from "<%= path_to_root %><%= command_generator.import_path %>/Result"
8
+ import { Error as <%= error_class_name %> } from "<%= path_to_root %><%= command_generator.import_path %>/Errors"
9
+
10
+ export default function <%= command_name %>Form (): JSX.Element {
11
+ <% non_colliding_inputs.each do |flattened_attribute| %>
12
+ <% if flattened_attribute.has_default? %>
13
+ const [<%= flattened_attribute.name %>, set<%= flattened_attribute.name_upcase %>] = useState<<%= flattened_attribute.ts_type %>>(<%= flattened_attribute.ts_default %>)
14
+ <% else %>
15
+ const [<%= flattened_attribute.name %>, set<%= flattened_attribute.name_upcase %>] = useState<<%= flattened_attribute.ts_type %> | undefined>(undefined)
16
+ <% end %>
17
+ <% end %>
18
+
19
+ const [result, setResult] = useState<string | null>(null)
20
+ const [error, setError] = useState<string | null>(null)
21
+
22
+ function toVoid (fn: () => Promise<void>): () => void {
23
+ return (): void => {
24
+ void (async (): Promise<void> => { await fn() })()
25
+ }
26
+ }
27
+
28
+ const run = toVoid(async (): Promise<void> => {
29
+ <% non_colliding_inputs.each do |flattened_attribute| %>
30
+ if (<%= flattened_attribute.name %> == null) {
31
+ // TODO: perform some kind of validation error
32
+ return
33
+ }
34
+ <% end %>
35
+
36
+ const inputs: <%= inputs_class_name %> = <%= populated_inputs_object %>
37
+
38
+ const command = new <%= command_name %>(inputs)
39
+
40
+ try {
41
+ setResult("Thinking...")
42
+ setError(null)
43
+ const outcome: Outcome<<%= result_class_name %>, <%= error_class_name %>> = await command.run()
44
+
45
+ if (outcome.isSuccess()) {
46
+ const result: <%= result_class_name %> = outcome.result
47
+ setResult(typeof result === 'string' ? result : JSON.stringify(result))
48
+ } else {
49
+ setError(outcome.errorMessage)
50
+ setResult(null)
51
+ }
52
+ } catch (error) {
53
+ setError('Error executing command')
54
+ setResult(null)
55
+ }
56
+ })
57
+
58
+ return (
59
+ <div className="CommandForm">
60
+ <div>
61
+ <% non_colliding_inputs.each do |flattened_attribute| %>
62
+ <%= flattened_attribute.html_input %>
63
+ <% end %>
64
+
65
+ <button onClick={run}><%= command_name_english %></button>
66
+ </div>
67
+
68
+ {(result != null) && <p>{result}</p>}
69
+ {(error != null) && <p className="error-message">{error}</p>}
70
+ </div>
71
+ )
72
+ }
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foobara-typescript-react-command-form-generator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Miles Georgi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: foobara
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'
27
+ - !ruby/object:Gem::Dependency
28
+ name: foobara-files-generator
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: foobara-typescript-remote-command-generator
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - azimux@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/foobara/typescript_react_command_form_generator.rb
66
+ - src/generate_typescript_react_command_form.rb
67
+ - src/typescript_react_command_form_generator.rb
68
+ - src/write_typescript_react_command_form_to_disk.rb
69
+ - templates/CommandForm.tsx.erb
70
+ homepage: https://github.com/foobara/generators-typescript-react-command-form-generator
71
+ licenses:
72
+ - Apache-2.0
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://github.com/foobara/generators-typescript-react-command-form-generator
76
+ source_code_uri: https://github.com/foobara/generators-typescript-react-command-form-generator
77
+ changelog_uri: https://github.com/foobara/generators-typescript-react-command-form-generator/blob/main/CHANGELOG.md
78
+ rubygems_mfa_required: 'true'
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.2
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.4.10
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Generates Typescript React forms for Foobara remote commands
98
+ test_files: []