rbytes 0.0.1 → 0.1.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: 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