rbytes 0.0.1 → 0.1.0

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: a2b785fc9d54a6ca927a015009e6dc828afd264c5bd9f2055ce903aa4ebd1c07
4
+ data.tar.gz: c6de88c97011399b70b77ffdbf138ef46edd26a9889451e74d0a954da553eee5
5
5
  SHA512:
6
- metadata.gz: 0b1021dc75c1ae9c56732c385101d53e65f2d8500f72b37e001e685db01a4b713ec6cd9052c0d0959a7acde61902c52c487f314c580be600aec2153f60957d4b
7
- data.tar.gz: 84e337dcbb359ac314731982e0ae8ebcb0dd1e879df4eba8fc05699a47b7bb0fef83d3485b320d9c88aa5370d0d1862bfde59bdaa9b4e3e3f77f6565aa7b6f18
6
+ metadata.gz: a7ef023c07d2b51593664bfa42de8de319ecfaad698d586905fc4cb93751e0fa40db2c7c1878893b8c3bbff2208604c5f536ba0a33f61e46e4dee2418798ffdc
7
+ data.tar.gz: d90ee6b3cfc3e1e1548d4c34862e355be5e3bfb10b8909a669ff1cc3cc4d05a8c7d9adac9940ae09ef24336c1e10ae05a0f3b203ce7eadd3f58b4f8e540853a3
data/CHANGELOG.md CHANGED
@@ -2,4 +2,8 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.0 (2023-03-17) 🍀
6
+
7
+ - Initial release.
8
+
5
9
  [@palkan]: https://github.com/palkan
data/README.md CHANGED
@@ -6,24 +6,184 @@ 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
+ Ruby Bytes adds partial support to Thor/Rails templates. For that, you can use `#include` and `#render` methods:
57
+
58
+ ```erb
59
+ say "Welcome to a custom Rails template!"
60
+
61
+ <%= include "setup_gems" %>
62
+
63
+ file "config/initializers/my-gem.rb", <%= code("initializer.rb") %>
64
+ ```
65
+
66
+ The `#include` helper simply injects the contents of the partial into the resulting file.
67
+
68
+ The `#code` method allows you to inject dynamic contents depending on the local variables defined. For example, given the following template and a partial:
69
+
70
+ ```erb
71
+ # _anycable.yml.tt
72
+ development:
73
+ broadcast_adapter: <%= cable_adapter %>
74
+
75
+ # template.rb
76
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
77
+
78
+ file "config/anycable.yml", <%= code("anycable.yml") %>
79
+ ```
80
+
81
+ The compiled template will like like this:
82
+
83
+ ```erb
84
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
85
+
86
+ file "config/anycable.yml", ERB.new(
87
+ *[
88
+ <<~'CODE'
89
+ development:
90
+ broadcast_adapter: <%= cable_adapter %>
91
+ CODE
92
+ ], trim_mode: "<>").result(binding)
93
+ ```
94
+
95
+ **NOTE:** By default, we assume that partials are stored next to the template's entrypoint. Partials may have "_" prefix and ".rb"/".tt" suffixes.
96
+
97
+ ### Compiling templates
98
+
99
+ You can compile a template by using the `rbytes` executable:
100
+
101
+ ```sh
102
+ $ rbytes compile path/to/template
103
+
104
+ <compiled template>
105
+ ```
106
+
107
+ You can also specify a custom partials directory:
108
+
109
+ ```sh
110
+ rbytes compile path/to/template --root=path/to/partials
111
+ ```
24
112
 
25
113
  ### Testing
26
114
 
115
+ We provide a Minitest integration to test your templates.
116
+
117
+ Here is an example usage:
118
+
119
+ ```ruby
120
+ require "ruby_bytes/test_case"
121
+
122
+ class TemplateTest < RubyBytes::TestCasee
123
+ # Specify root path for your template (for partials lookup)
124
+ root File.join(__dir__, "../template")
125
+
126
+ # You can test partials in isolation by declaring a custom template
127
+ template <<~RUBY
128
+ say "Hello from some partial"
129
+ <%= include "some_partial" %>
130
+ RUBY
131
+
132
+ def test_some_partial
133
+ run_generator do |output|
134
+ assert_file "application.rb"
135
+
136
+ assert_file_contains(
137
+ "application.rb",
138
+ <<~CODE
139
+ module Rails
140
+ class << self
141
+ def application
142
+ CODE
143
+ )
144
+
145
+ refute_file_contains(
146
+ "application.rb",
147
+ "Nothing"
148
+ )
149
+
150
+ assert_line_printed output, "Hello from some partial"
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ If you use prompt in your templates, you can prepopulate standard input:
157
+
158
+ ```ruby
159
+ class TemplateTest < RubyBytes::TestCasee
160
+ # Specify root path for your template (for partials lookup)
161
+ root File.join(__dir__, "../template")
162
+
163
+ # You can test partials in isolation by declaring a custom template
164
+ template <<~RUBY
165
+ say "Hello from some partial"
166
+ if yes?("Do you write tests?")
167
+ say "Gut"
168
+ else
169
+ say "Why not?"
170
+ end
171
+ RUBY
172
+
173
+ def test_prompt_yes
174
+ run_generator(input: ["y"]) do |output|
175
+ assert_line_printed output, "Gut"
176
+ end
177
+ end
178
+
179
+ def test_prompt_no
180
+ run_generator(input: ["n"]) do |output|
181
+ assert_line_printed output, "Why not?"
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
27
187
  ## Thor integration
28
188
 
29
189
  We provide a custom Thor command, which can be used to apply templates (similar to `rails app:template`).
@@ -46,6 +206,43 @@ Run template from: https://railsbytes.com/script/x7msKX
46
206
  hello world from https://railsbytes.com 👋
47
207
  ```
48
208
 
209
+ ## GitHub action
210
+
211
+ You can use our GitHub action to deploy your templates to RailsBytes.
212
+
213
+ Here is an example:
214
+
215
+ ```yml
216
+ name: Publish
217
+
218
+ on:
219
+ push:
220
+ tags:
221
+ - v*
222
+ workflow_dispatch:
223
+
224
+ jobs:
225
+ publish:
226
+ uses: palkan/rbytes/.github/workflows/railsbytes.yml@master
227
+ with:
228
+ template: templates/my-template.rb
229
+ secrets:
230
+ RAILS_BYTES_ACCOUNT_ID: "${{ secrets.RAILS_BYTES_ACCOUNT_ID }}"
231
+ RAILS_BYTES_TOKEN: "${{ secrets.RAILS_BYTES_TOKEN }}"
232
+ RAILS_BYTES_TEMPLATE_ID: "${{ secrets.RAILS_TEMPLATE_ID }}"
233
+ ```
234
+
235
+ ## Publishing manually
236
+
237
+ You can use the `rbytes publish` command to compile and publish a template to RailsBytes:
238
+
239
+ ```sh
240
+ RAILS_BYTES_ACCOUNT_ID=aaa \
241
+ RAILS_BYTES_TOKEN=bbb \
242
+ RAILS_BYTES_TEMPLATE_ID=ccc \
243
+ rbytes publish path/to/template
244
+ ```
245
+
49
246
  ## Contributing
50
247
 
51
248
  Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/rbytes](https://github.com/palkan/rbytes).
@@ -59,3 +256,5 @@ This gem is generated via [new-gem-generator](https://github.com/palkan/new-gem-
59
256
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
60
257
 
61
258
  [RailsBytes]: https://railsbytes.com
259
+ [ruby-on-whales]: https://github.com/evilmartians/ruby-on-whales
260
+ [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,116 @@
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.new.template(url)
114
+ end
115
+ end
116
+ 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
+ <<~'CODE'
26
+ #{contents}
27
+ CODE
28
+ ], trim_mode: "<>").result(binding))
29
+ end
30
+
31
+ def include(path, indent: 0)
32
+ indented(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,180 @@
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
+ FileUtils.rm_rf(TMP_DIR) if File.directory?(TMP_DIR)
15
+ FileUtils.mkdir_p(TMP_DIR)
16
+
17
+ Rbytes::Base.source_paths << TMP_DIR
18
+
19
+ # Patch Thor::LineEditor to use Basic in tests
20
+ $rbytes_testing = false
21
+
22
+ Thor::LineEditor.singleton_class.prepend(Module.new do
23
+ def best_available
24
+ return super unless $rbytes_testing
25
+
26
+ Thor::LineEditor::Basic
27
+ end
28
+ end)
29
+
30
+ class << self
31
+ attr_reader :template_contents
32
+
33
+ def destination_root(val = nil)
34
+ if val
35
+ @destination_root = val
36
+ end
37
+
38
+ return @destination_root if instance_variable_defined?(:@destination_root)
39
+
40
+ @destination_root =
41
+ if superclass.respond_to?(:destination_root)
42
+ superclass.destination_root
43
+ else
44
+ TMP_DIR
45
+ end
46
+ end
47
+
48
+ def root(val = nil)
49
+ if val
50
+ @root = val
51
+ end
52
+
53
+ return @root if instance_variable_defined?(:@root)
54
+
55
+ @root =
56
+ if superclass.respond_to?(:root)
57
+ superclass.root
58
+ end
59
+ end
60
+
61
+ # Set the path to dummy app.
62
+ # Dummy app is copied to the temporary directory for every run
63
+ # and set as a destination root.
64
+ def dummy_app(val = nil)
65
+ if val
66
+ @dummy_app = val
67
+ end
68
+
69
+ return @dummy_app if instance_variable_defined?(:@dummy_app)
70
+
71
+ @dummy_app =
72
+ if superclass.respond_to?(:dummy_app)
73
+ superclass.dummy_app
74
+ end
75
+ end
76
+
77
+ def template(contents)
78
+ @template_contents = contents
79
+ end
80
+ end
81
+
82
+ attr_accessor :destination
83
+
84
+ def prepare_dummy
85
+ # Then, copy the dummy app if any
86
+ dummy = self.class.dummy_app
87
+ return unless dummy
88
+
89
+ return if @dummy_prepared
90
+
91
+ raise ArgumentError, "Dummy app must be a directory" unless File.directory?(dummy)
92
+
93
+ tmp_dummy_path = File.join(TMP_DIR, "dummy")
94
+ FileUtils.rm_rf(tmp_dummy_path) if File.directory?(tmp_dummy_path)
95
+ FileUtils.cp_r(dummy, tmp_dummy_path)
96
+ self.destination = tmp_dummy_path
97
+
98
+ if block_given?
99
+ Dir.chdir(tmp_dummy_path) { yield }
100
+ end
101
+
102
+ @dummy_prepared = true
103
+ end
104
+
105
+ def run_generator(input: [])
106
+ # First, compile the template (if not yet)
107
+ path = File.join(TMP_DIR, "current_template.rb")
108
+
109
+ if File.file?(path)
110
+ File.delete(path)
111
+ end
112
+
113
+ File.write(path, "")
114
+
115
+ rendered = Compiler.new(path, root: self.class.root).render(self.class.template_contents)
116
+ File.write(path, rendered)
117
+
118
+ self.destination = self.class.destination_root
119
+
120
+ prepare_dummy
121
+
122
+ original_stdout = $stdout
123
+ original_stdin = $stdin
124
+
125
+ if input.size > 0
126
+ $rbytes_testing = true
127
+
128
+ io = StringIO.new
129
+ input.each { io.puts(_1) }
130
+ io.rewind
131
+ $stdin = io
132
+ $stdin.sync = true
133
+ end
134
+
135
+ $stdout = StringIO.new
136
+ $stdout.sync = true
137
+
138
+ begin
139
+ Dir.chdir(destination) do
140
+ Rbytes::Base.new(
141
+ [destination], {}, {destination_root: destination}
142
+ ).apply("current_template.rb")
143
+ end
144
+ yield $stdout.string if block_given?
145
+ ensure
146
+ $stdout = original_stdout
147
+ $stdin = original_stdin
148
+ $rbytes_testing = false
149
+ @dummy_prepared = false
150
+ end
151
+ end
152
+
153
+ def assert_line_printed(io, line)
154
+ lines = io.lines
155
+
156
+ assert lines.any? { _1.include?(line) }, "Expected to print line: #{line}. Got: #{io}"
157
+ end
158
+
159
+ def assert_file_contains(path, body)
160
+ fullpath = File.join(destination, path)
161
+ assert File.file?(fullpath), "File not found: #{path}"
162
+
163
+ actual = File.read(fullpath)
164
+ assert_includes actual, body
165
+ end
166
+
167
+ def assert_file(path)
168
+ fullpath = File.join(destination, path)
169
+ assert File.file?(fullpath), "File not found: #{path}"
170
+ end
171
+
172
+ def refute_file_contains(path, body)
173
+ fullpath = File.join(destination, path)
174
+ assert File.file?(fullpath), "File not found: #{path}"
175
+
176
+ actual = File.read(fullpath)
177
+ refute_includes actual, body
178
+ end
179
+ end
180
+ end