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

Sign up to get free protection for your applications and to get access to all the features.
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: []