rbytes 0.0.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2018b6cc0392420457e2edd1a8ec027f55978148bbb4ea5fba8e517116dc166
4
- data.tar.gz: de96f8614e325160cda52825af368626ba6f70769dc1323efab289a5023c47d0
3
+ metadata.gz: 53595c57bee23eca0389d923a2b7fec3c67e570c07a4405cc705afe658081a3a
4
+ data.tar.gz: d96f2d9536487f2432a0f56c9d9407c6caa7f04a2d9e98ecea72111e3e88e9b0
5
5
  SHA512:
6
- metadata.gz: 0b1021dc75c1ae9c56732c385101d53e65f2d8500f72b37e001e685db01a4b713ec6cd9052c0d0959a7acde61902c52c487f314c580be600aec2153f60957d4b
7
- data.tar.gz: 84e337dcbb359ac314731982e0ae8ebcb0dd1e879df4eba8fc05699a47b7bb0fef83d3485b320d9c88aa5370d0d1862bfde59bdaa9b4e3e3f77f6565aa7b6f18
6
+ metadata.gz: 9bee2dcc4ecf593b7aa7d7289955eb4518dadf74087bafa523880647e193bd2b8d16af2542c27e73f2a6d05fcbd2a73ea5de2fef02bfdd202dc013d4024c147a
7
+ data.tar.gz: 8f5928ca9ba174fab8e830825f2bad113fbb295cbd84b106cd0364d0519037750c1343acc19b40f1d1152f7ec1d92760180d594730247ff4c16d4133560c6376
data/CHANGELOG.md CHANGED
@@ -2,4 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.1 (2023-03-20)
6
+
7
+ - Add Ruby Bytes project generator (see `templates/generator`).
8
+
9
+ ## 0.1.0 (2023-03-17) 🍀
10
+
11
+ - Initial release.
12
+
5
13
  [@palkan]: https://github.com/palkan
data/README.md CHANGED
@@ -6,24 +6,186 @@ Ruby Bytes is a tool to build application templates for Ruby and Rails applicati
6
6
 
7
7
  - Build complex templates consisting of multiple independent components.
8
8
  - Test templates with ease.
9
+ - Install application templates without Rails.
10
+ - Publish templates to [RailsBytes][].
9
11
 
10
- We also provide a GitHub action to deploy _compiled_ templates to [RailsBytes][].
12
+ We also provide a [GitHub action](#github-action) to compile and deploy templates continuously.
11
13
 
12
- Templates built with Ruby Bytes can be used with the `rails app:template` command or with a custom [Thor command](#thor-integration) (if you want to use a template in a Rails-less environment)
14
+ ## Examples
15
+
16
+ - [ruby-on-whales][]
17
+ - [view_component-contrib][]
18
+
19
+ See also examples in the [templates](https://github.com/palkan/rbytes/tree/master/templates) folder.
13
20
 
14
21
  ## Installation
15
22
 
16
- In your Gemfile:
23
+ To install templates, install the `rbytes` executable via the gem:
24
+
25
+ ```sh
26
+ gem install rbytes
27
+ ```
28
+
29
+ For templates development, add `rbytes` to your Gemfile or gemspec:
17
30
 
18
31
  ```ruby
19
32
  # Gemfile
20
33
  gem "rbytes"
21
34
  ```
22
35
 
23
- ## Building templates
36
+ ## Installing templates
37
+
38
+ You can use `rbytes install <url>` similarly to `rails app:template` but without needing to install Rails. It's useful if you want to use a template in a Rails-less environment.
39
+
40
+ Usage example:
41
+
42
+ ```sh
43
+ $ rbytes install https://railsbytes.com/script/x7msKX
44
+
45
+ Run template from: https://railsbytes.com/script/x7msKX
46
+ apply https://railsbytes.com/script/x7msKX
47
+ hello world from https://railsbytes.com 👋
48
+ ```
49
+
50
+ **IMPORTANT**: Not all templates from RailsBytes may be supported as of yet. Please, let us know if you find incompatibilities with `rails app:template`, so we can fix them.
51
+
52
+ You can also install Ruby Bytes as a plugin for Thor (see [Thor integration](#thor-integration)).
53
+
54
+ ## Writing templates
55
+
56
+ > The quickes way to get started with using Ruby Bytes to build templates is to use [our generator](templates/generator/).
57
+
58
+ Ruby Bytes adds partial support to Thor/Rails templates. For that, you can use `#include` and `#render` methods:
59
+
60
+ ```erb
61
+ say "Welcome to a custom Rails template!"
62
+
63
+ <%= include "setup_gems" %>
64
+
65
+ file "config/initializers/my-gem.rb", <%= code("initializer.rb") %>
66
+ ```
67
+
68
+ The `#include` helper simply injects the contents of the partial into the resulting file.
69
+
70
+ The `#code` method allows you to inject dynamic contents depending on the local variables defined. For example, given the following template and a partial:
71
+
72
+ ```erb
73
+ # _anycable.yml.tt
74
+ development:
75
+ broadcast_adapter: <%= cable_adapter %>
76
+
77
+ # template.rb
78
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
79
+
80
+ file "config/anycable.yml", <%= code("anycable.yml") %>
81
+ ```
82
+
83
+ The compiled template will like like this:
84
+
85
+ ```erb
86
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
87
+
88
+ file "config/anycable.yml", ERB.new(
89
+ *[
90
+ <<~'CODE'
91
+ development:
92
+ broadcast_adapter: <%= cable_adapter %>
93
+ CODE
94
+ ], trim_mode: "<>").result(binding)
95
+ ```
96
+
97
+ **NOTE:** By default, we assume that partials are stored next to the template's entrypoint. Partials may have "_" prefix and ".rb"/".tt" suffixes.
98
+
99
+ ### Compiling templates
100
+
101
+ You can compile a template by using the `rbytes` executable:
102
+
103
+ ```sh
104
+ $ rbytes compile path/to/template
105
+
106
+ <compiled template>
107
+ ```
108
+
109
+ You can also specify a custom partials directory:
110
+
111
+ ```sh
112
+ rbytes compile path/to/template --root=path/to/partials
113
+ ```
24
114
 
25
115
  ### Testing
26
116
 
117
+ We provide a Minitest integration to test your templates.
118
+
119
+ Here is an example usage:
120
+
121
+ ```ruby
122
+ require "ruby_bytes/test_case"
123
+
124
+ class TemplateTest < RubyBytes::TestCasee
125
+ # Specify root path for your template (for partials lookup)
126
+ root File.join(__dir__, "../template")
127
+
128
+ # You can test partials in isolation by declaring a custom template
129
+ template <<~RUBY
130
+ say "Hello from some partial"
131
+ <%= include "some_partial" %>
132
+ RUBY
133
+
134
+ def test_some_partial
135
+ run_generator do |output|
136
+ assert_file "application.rb"
137
+
138
+ assert_file_contains(
139
+ "application.rb",
140
+ <<~CODE
141
+ module Rails
142
+ class << self
143
+ def application
144
+ CODE
145
+ )
146
+
147
+ refute_file_contains(
148
+ "application.rb",
149
+ "Nothing"
150
+ )
151
+
152
+ assert_line_printed output, "Hello from some partial"
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ If you use prompt in your templates, you can prepopulate standard input:
159
+
160
+ ```ruby
161
+ class TemplateTest < RubyBytes::TestCasee
162
+ # Specify root path for your template (for partials lookup)
163
+ root File.join(__dir__, "../template")
164
+
165
+ # You can test partials in isolation by declaring a custom template
166
+ template <<~RUBY
167
+ say "Hello from some partial"
168
+ if yes?("Do you write tests?")
169
+ say "Gut"
170
+ else
171
+ say "Why not?"
172
+ end
173
+ RUBY
174
+
175
+ def test_prompt_yes
176
+ run_generator(input: ["y"]) do |output|
177
+ assert_line_printed output, "Gut"
178
+ end
179
+ end
180
+
181
+ def test_prompt_no
182
+ run_generator(input: ["n"]) do |output|
183
+ assert_line_printed output, "Why not?"
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
27
189
  ## Thor integration
28
190
 
29
191
  We provide a custom Thor command, which can be used to apply templates (similar to `rails app:template`).
@@ -46,6 +208,43 @@ Run template from: https://railsbytes.com/script/x7msKX
46
208
  hello world from https://railsbytes.com 👋
47
209
  ```
48
210
 
211
+ ## GitHub action
212
+
213
+ You can use our GitHub action to deploy your templates to RailsBytes.
214
+
215
+ Here is an example:
216
+
217
+ ```yml
218
+ name: Publish
219
+
220
+ on:
221
+ push:
222
+ tags:
223
+ - v*
224
+ workflow_dispatch:
225
+
226
+ jobs:
227
+ publish:
228
+ uses: palkan/rbytes/.github/workflows/railsbytes.yml@master
229
+ with:
230
+ template: templates/my-template.rb
231
+ secrets:
232
+ RAILS_BYTES_ACCOUNT_ID: "${{ secrets.RAILS_BYTES_ACCOUNT_ID }}"
233
+ RAILS_BYTES_TOKEN: "${{ secrets.RAILS_BYTES_TOKEN }}"
234
+ RAILS_BYTES_TEMPLATE_ID: "${{ secrets.RAILS_TEMPLATE_ID }}"
235
+ ```
236
+
237
+ ## Publishing manually
238
+
239
+ You can use the `rbytes publish` command to compile and publish a template to RailsBytes:
240
+
241
+ ```sh
242
+ RAILS_BYTES_ACCOUNT_ID=aaa \
243
+ RAILS_BYTES_TOKEN=bbb \
244
+ RAILS_BYTES_TEMPLATE_ID=ccc \
245
+ rbytes publish path/to/template
246
+ ```
247
+
49
248
  ## Contributing
50
249
 
51
250
  Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/rbytes](https://github.com/palkan/rbytes).
@@ -59,3 +258,5 @@ This gem is generated via [new-gem-generator](https://github.com/palkan/new-gem-
59
258
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
60
259
 
61
260
  [RailsBytes]: https://railsbytes.com
261
+ [ruby-on-whales]: https://github.com/evilmartians/ruby-on-whales
262
+ [view_component-contrib]: https://github.com/palkan/view_component-contrib
data/bin/rbytes ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ruby_bytes/cli"
4
+
5
+ begin
6
+ cli = RubyBytes::CLI.new
7
+ cli.run(*ARGV)
8
+ rescue => e
9
+ raise e if $DEBUG
10
+ STDERR.puts e.message
11
+ STDERR.puts e.backtrace.join("\n") if $DEBUG
12
+ exit 1
13
+ end
data/lib/rbytes.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rbytes/version"
3
+ require "ruby_bytes/version"
4
+ require "ruby_bytes/compiler"
5
+ require "ruby_bytes/publisher"
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbytes"
4
+ require "optparse"
5
+
6
+ module RubyBytes
7
+ class CLI
8
+ COMMANDS = %w[
9
+ compile
10
+ publish
11
+ install
12
+ ].freeze
13
+
14
+ def run(command, *args)
15
+ raise ArgumentError, "Unknown command: #{command}\nAvailable commands are: #{COMMANDS.join(", ")}\nRuby Bytes: v#{RubyBytes::VERSION}" unless COMMANDS.include?(command)
16
+
17
+ public_send(command, *args)
18
+ end
19
+
20
+ def compile(*args)
21
+ root = nil # rubocop:disable Lint/UselessAssignment
22
+
23
+ path, args = *args
24
+
25
+ OptionParser.new do |o|
26
+ o.on "-v", "--version", "Print version and exit" do |_arg|
27
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
28
+ exit(0)
29
+ end
30
+
31
+ o.on "--root [DIR]", "Location of partial template files" do
32
+ raise ArgumentError, "Directory not found: #{_1}" unless File.directory?(_1)
33
+ root = _1
34
+ end
35
+
36
+ o.on_tail "-h", "--help", "Show help" do
37
+ $stdout.puts <<~USAGE
38
+ rbytes compile PATH [options]
39
+
40
+ Options:
41
+ --root DIR Location of partial template files
42
+ USAGE
43
+
44
+ exit(0)
45
+ end
46
+ end.parse!(args || [])
47
+
48
+ raise ArgumentError, "File not found: #{path}" unless File.file?(path)
49
+
50
+ $stdout.puts Compiler.new(path, root: root).render
51
+ end
52
+
53
+ def publish(*args)
54
+ root = nil # rubocop:disable Lint/UselessAssignment
55
+
56
+ path, args = *args
57
+
58
+ OptionParser.new do |o|
59
+ o.on "-v", "--version", "Print version and exit" do |_arg|
60
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
61
+ exit(0)
62
+ end
63
+
64
+ o.on "--root [DIR]", "Location of partial template files" do
65
+ raise ArgumentError, "Directory not found: #{_1}" unless File.directory?(_1)
66
+ root = _1
67
+ end
68
+
69
+ o.on_tail "-h", "--help", "Show help" do
70
+ $stdout.puts <<~USAGE
71
+ rbytes publish PATH [options]
72
+
73
+ Options:
74
+ --root DIR Location of partial template files
75
+ USAGE
76
+
77
+ exit(0)
78
+ end
79
+ end.parse!(args || [])
80
+
81
+ raise ArgumentError, "File not found: #{path}" unless File.file?(path)
82
+
83
+ contents = Compiler.new(path, root: root).render
84
+
85
+ Publisher.new.call(contents)
86
+
87
+ $stdout.puts "Published successfully ✅"
88
+ end
89
+
90
+ def install(*args)
91
+ url, args = *args
92
+
93
+ OptionParser.new do |o|
94
+ o.on "-v", "--version", "Print version and exit" do |_arg|
95
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
96
+ exit(0)
97
+ end
98
+
99
+ o.on_tail "-h", "--help", "Show help" do
100
+ $stdout.puts <<~USAGE
101
+ rbytes install URL
102
+ USAGE
103
+
104
+ exit(0)
105
+ end
106
+ end.parse!(args || [])
107
+
108
+ raise ArgumentError, "Template URL or location must be provided" unless url
109
+
110
+ require "thor"
111
+ require "ruby_bytes/thor"
112
+
113
+ Rbytes::Base.source_paths << Dir.pwd
114
+ Rbytes.new.template(url)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module RubyBytes
6
+ class Compiler
7
+ attr_reader :path, :template, :root
8
+
9
+ def initialize(path, root: nil)
10
+ @path = path
11
+ raise ArgumentError, "There is no file at the path: #{path}" unless File.file?(path)
12
+
13
+ @template = File.read(path)
14
+ @root = root || File.dirname(File.expand_path(path))
15
+ end
16
+
17
+ def render(contents = template)
18
+ ERB.new(contents, trim_mode: "<>").result(binding)
19
+ end
20
+
21
+ def code(path)
22
+ contents = File.read(resolve_path(path))
23
+ %(ERB.new(
24
+ *[
25
+ <<~'TCODE'
26
+ #{contents}
27
+ TCODE
28
+ ], trim_mode: "<>").result(binding))
29
+ end
30
+
31
+ def include(path, indent: 0)
32
+ indented(render(File.read(resolve_path(path))), indent)
33
+ end
34
+
35
+ private
36
+
37
+ PATH_CANDIDATES = [
38
+ "%{path}",
39
+ "_%{path}",
40
+ "%{path}.rb",
41
+ "_%{path}.rb",
42
+ "%{path}.tt",
43
+ "_%{path}.tt"
44
+ ].freeze
45
+
46
+ def resolve_path(path)
47
+ PATH_CANDIDATES.each do |pattern|
48
+ resolved = File.join(root, pattern % {path: path})
49
+ return resolved if File.file?(resolved)
50
+ end
51
+
52
+ raise "File not found: #{path}"
53
+ end
54
+
55
+ def indented(content, multiplier = 2) # :doc:
56
+ spaces = " " * multiplier
57
+ content.each_line.map { |line| (!line.match?(/\S/)) ? line : "#{spaces}#{line}" }.join
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyBytes
4
+ class Publisher
5
+ attr_reader :account_id, :token, :template_id
6
+
7
+ def initialize(
8
+ account_id: ENV.fetch("RAILS_BYTES_ACCOUNT_ID"),
9
+ token: ENV.fetch("RAILS_BYTES_TOKEN"),
10
+ template_id: ENV.fetch("RAILS_BYTES_TEMPLATE_ID")
11
+ )
12
+ @account_id = account_id
13
+ @token = token
14
+ @template_id = template_id
15
+ end
16
+
17
+ def call(template)
18
+ require "net/http"
19
+ require "json"
20
+
21
+ path = "/api/v1/accounts/#{account_id}/templates/#{template_id}.json"
22
+ data = JSON.dump(script: template)
23
+
24
+ Net::HTTP.start("railsbytes.com", 443, use_ssl: true) do |http|
25
+ http.patch(
26
+ path,
27
+ data,
28
+ {
29
+ "Content-Type" => "application/json",
30
+ "Authorization" => "Bearer GGpxArFDSa2x3MT4BQNcyxG6"
31
+ }
32
+ )
33
+ end.then do |response|
34
+ raise "Failed to publish template: #{response.code} — #{response.message}" unless response.code == "200"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "fileutils"
5
+ require "stringio"
6
+ require "thor"
7
+
8
+ require "ruby_bytes/thor"
9
+
10
+ module RubyBytes
11
+ class TestCase < Minitest::Test
12
+ TMP_DIR = File.join(Dir.pwd, "tmp", "rbytes_test")
13
+
14
+ Rbytes::Base.source_paths << TMP_DIR
15
+
16
+ # Patch Thor::LineEditor to use Basic in tests
17
+ $rbytes_testing = false
18
+
19
+ Thor::LineEditor.singleton_class.prepend(Module.new do
20
+ def best_available
21
+ return super unless $rbytes_testing
22
+
23
+ Thor::LineEditor::Basic
24
+ end
25
+ end)
26
+
27
+ class << self
28
+ attr_reader :template_contents
29
+
30
+ def destination_root(val = nil)
31
+ if val
32
+ @destination_root = val
33
+ end
34
+
35
+ return @destination_root if instance_variable_defined?(:@destination_root)
36
+
37
+ @destination_root =
38
+ if superclass.respond_to?(:destination_root)
39
+ superclass.destination_root
40
+ else
41
+ TMP_DIR
42
+ end
43
+ end
44
+
45
+ def root(val = nil)
46
+ if val
47
+ @root = val
48
+ end
49
+
50
+ return @root if instance_variable_defined?(:@root)
51
+
52
+ @root =
53
+ if superclass.respond_to?(:root)
54
+ superclass.root
55
+ end
56
+ end
57
+
58
+ # Set the path to dummy app.
59
+ # Dummy app is copied to the temporary directory for every run
60
+ # and set as a destination root.
61
+ def dummy_app(val = nil)
62
+ if val
63
+ @dummy_app = val
64
+ end
65
+
66
+ return @dummy_app if instance_variable_defined?(:@dummy_app)
67
+
68
+ @dummy_app =
69
+ if superclass.respond_to?(:dummy_app)
70
+ superclass.dummy_app
71
+ end
72
+ end
73
+
74
+ def template(contents)
75
+ @template_contents = contents
76
+ end
77
+ end
78
+
79
+ attr_accessor :destination
80
+
81
+ def setup
82
+ FileUtils.rm_rf(TMP_DIR) if File.directory?(TMP_DIR)
83
+ FileUtils.mkdir_p(TMP_DIR)
84
+ end
85
+
86
+ def prepare_dummy
87
+ # Then, copy the dummy app if any
88
+ dummy = self.class.dummy_app
89
+ return unless dummy
90
+
91
+ return if @dummy_prepared
92
+
93
+ raise ArgumentError, "Dummy app must be a directory" unless File.directory?(dummy)
94
+
95
+ tmp_dummy_path = File.join(TMP_DIR, "dummy")
96
+ FileUtils.rm_rf(tmp_dummy_path) if File.directory?(tmp_dummy_path)
97
+ FileUtils.cp_r(dummy, tmp_dummy_path)
98
+ self.destination = tmp_dummy_path
99
+
100
+ if block_given?
101
+ Dir.chdir(tmp_dummy_path) { yield }
102
+ end
103
+
104
+ @dummy_prepared = true
105
+ end
106
+
107
+ def run_generator(input: [])
108
+ # First, compile the template (if not yet)
109
+ path = File.join(TMP_DIR, "current_template.rb")
110
+
111
+ if File.file?(path)
112
+ File.delete(path)
113
+ end
114
+
115
+ File.write(path, "")
116
+
117
+ rendered = Compiler.new(path, root: self.class.root).render(self.class.template_contents)
118
+ File.write(path, rendered)
119
+
120
+ self.destination = self.class.destination_root
121
+
122
+ prepare_dummy
123
+
124
+ original_stdout = $stdout
125
+ original_stdin = $stdin
126
+
127
+ if input.size > 0
128
+ $rbytes_testing = true
129
+
130
+ io = StringIO.new
131
+ input.each { io.puts(_1) }
132
+ io.rewind
133
+ $stdin = io
134
+ $stdin.sync = true
135
+ end
136
+
137
+ $stdout = StringIO.new
138
+ $stdout.sync = true
139
+
140
+ begin
141
+ Dir.chdir(destination) do
142
+ Rbytes::Base.new(
143
+ [destination], {}, {destination_root: destination}
144
+ ).apply("current_template.rb")
145
+ end
146
+ yield $stdout.string if block_given?
147
+ ensure
148
+ $stdout = original_stdout
149
+ $stdin = original_stdin
150
+ $rbytes_testing = false
151
+ @dummy_prepared = false
152
+ end
153
+ end
154
+
155
+ def assert_line_printed(io, line)
156
+ lines = io.lines
157
+
158
+ assert lines.any? { _1.include?(line) }, "Expected to print line: #{line}. Got: #{io}"
159
+ end
160
+
161
+ def assert_file_contains(path, body)
162
+ fullpath = File.join(destination, path)
163
+ assert File.file?(fullpath), "File not found: #{path}"
164
+
165
+ actual = File.read(fullpath)
166
+ assert_includes actual, body
167
+ end
168
+
169
+ def assert_file(path)
170
+ fullpath = File.join(destination, path)
171
+ assert File.file?(fullpath), "File not found: #{path}"
172
+ end
173
+
174
+ def refute_file(path)
175
+ fullpath = File.join(destination, path)
176
+ refute File.file?(fullpath), "File must not exist: #{path}"
177
+ end
178
+
179
+ def refute_file_contains(path, body)
180
+ fullpath = File.join(destination, path)
181
+ assert File.file?(fullpath), "File not found: #{path}"
182
+
183
+ actual = File.read(fullpath)
184
+ refute_includes actual, body
185
+ end
186
+ end
187
+ end