Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generators #110

Merged
merged 6 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .dockerdev/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
version: '3.4'
version: '2.4'

services:
app:
image: ruby:2.6
environment:
- GEM_HOME=/bundle
- BUNDLE_PATH=/bundle
- HISTFILE=/app/tmp/.bash_history
working_dir: /app
volumes:
- ..:/app
- bundle:/bundle
- gems:/usr/local/bundle

volumes:
bundle:
gems:
10 changes: 10 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,39 @@ Style/BlockDelimiters:
Metrics/MethodLength:
Exclude:
- 'spec/**/*.rb'
- 'lib/rails/generators/**/*.rb'

Metrics/LineLength:
Max: 100
Exclude:
- 'spec/**/*.rb'
- 'lib/rails/generators/**/*.rb'

Metrics/AbcSize:
Exclude:
- 'spec/**/*.rb'
- 'lib/rails/generators/**/*.rb'

Metrics/BlockLength:
Exclude:
- '*.gemspec'
- 'spec/**/*.rb'
- 'anycable-rails.gemspec'
- 'lib/rails/generators/**/*.rb'

Metrics/ClassLength:
Exclude:
- 'lib/rails/generators/**/*.rb'

Metrics/CyclomaticComplexity:
Exclude:
- 'spec/**/*.rb'
- 'lib/rails/generators/**/*.rb'

Metrics/PerceivedComplexity:
Exclude:
- 'spec/**/*.rb'
- 'lib/rails/generators/**/*.rb'

Lint/HandleExceptions:
Enabled: false
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master

- Add set up generator to configure a Rails application by running `bin/rails g anycable:setup`. ([@bibendi][])

- Require a minimum version of Ruby when installing the gem. ([@bibendi][])

- Add ability to develop the gem with Docker. ([@bibendi][])
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,25 @@ gem "redis", ">= 4.0"

(and don't forget to run `bundle install`).

Next, specify AnyCable subscription adapter for Action Cable:
### Interactive set up

After gem was installed, you can run set up wizard to configure a Rails application by going through a few questions:

```sh
bin/rails g anycable:setup
```

### Manual set up

Specify AnyCable subscription adapter for Action Cable:

```yml
# config/cable.yml
production:
development:
adapter: any_cable # or anycable

production:
adapter: any_cable
```

and specify AnyCable WebSocket server URL:
Expand Down
3 changes: 2 additions & 1 deletion anycable-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ Gem::Specification.new do |spec|
"source_code_uri" => "http://github.com/anycable/anycable-rails"
}

spec.files = `git ls-files README.md MIT-LICENSE CHANGELOG.md lib`.split
spec.files = Dir.glob("lib/**/*") + %w[README.md MIT-LICENSE CHANGELOG.md]
spec.require_paths = ["lib"]

spec.required_ruby_version = ">= 2.4"

spec.add_dependency "anycable", "~> 0.6.0"
spec.add_dependency "rails", ">= 5"

spec.add_development_dependency "ammeter", "~> 1.1"
spec.add_development_dependency "bundler", ">= 1.10"
spec.add_development_dependency "pry-byebug"
spec.add_development_dependency "rake", "~> 10.0"
Expand Down
235 changes: 235 additions & 0 deletions lib/rails/generators/anycable/setup/setup_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# frozen_string_literal: true

module AnyCableRailsGenerators
# Entry point for interactive installation
class SetupGenerator < ::Rails::Generators::Base
namespace "anycable:setup"
source_root File.expand_path("templates", __dir__)

METHODS = %w[skip local docker].freeze
SERVER_VERSION = "v0.6.4"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely need to not use a server version in a constant. Otherwise, we will forget to change that constant every time after the server was released.
There is a secret url that can get the latest asset from the latest release.
https://github.com/anycable/anycable-go/releases/latest/download/anycable-go-v0.6.4-linux-amd64
The big problem we have is that a binary contains a version in a name. This is not the necessary information. I propose to cut it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@palkan What do think? Can we roll out a new release with new naming to have the ability to change the version to the latest here?

Copy link
Member

@palkan palkan Nov 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good idea. Let's add a ticket to the v1.0 project.

For now, let's leave as is (let's add a TODO: or FIXME: with this comment), so we'll be able to test the generator without waiting for anycable-go fixes.

OS_NAMES = %w[linux darwin freebsd win].freeze
CPU_NAMES = %w[amd64 arm64 386 arm].freeze
SERVER_SOURCES = %w[skip brew binary].freeze
DEFAULT_BIN_PATH = "/usr/local/bin"

class_option :method,
type: :string,
desc: "Select your development environment to run particular installation method (options: #{METHODS.join(', ')})"
class_option :source,
type: :string,
desc: "Install AnyCable-Go server from particular source (options: #{SERVER_SOURCES.join(', ')})"
class_option :bin_path,
type: :string,
desc: "Install AnyCable-Go server binary to directory (default: #{DEFAULT_BIN_PATH})"
class_option :os,
type: :string,
desc: "Download AnyCable-Go server binary for particular OS (options: #{OS_NAMES.join(', ')})"
class_option :cpu,
type: :string,
desc: "Download AnyCable-Go server binary for particular CPU architecture (options: #{CPU_NAMES.join(', ')})"
class_option :skip_heroku,
type: :boolean,
desc: "Do not copy Heroku configs"
class_option :skip_procfile_dev,
type: :boolean,
desc: "Do not create Procfile.dev"

def welcome
say "👋 Welcome to AnyCable interactive installer."
end

def configs
inside("config") do
template "cable.yml"
template "anycable.yml"
end
end

def cable_url
environment(nil, env: :development) do
<<~SNIPPET
# Specify AnyCable WebSocket server URL to use by JS client
config.action_cable.url = ENV.fetch("CABLE_URL", "ws://localhost:3334/cable").presence
SNIPPET
end

environment(nil, env: :production) do
<<~SNIPPET
# Specify AnyCable WebSocket server URL to use by JS client
config.action_cable.url = ENV["CABLE_URL"].presence
SNIPPET
end

say_status :info, "✅ 'config.action_cable.url' has been configured"
say_status :help, "️️⚠️ Make sure you have `action_cable_meta_tag` in your application.html if you're using JS client"
end

def development_method
answer = METHODS.index(options[:method]) || 99

until METHODS[answer.to_i]
answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip"
end

case env = METHODS[answer.to_i]
when "skip"
say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
else
send "install_for_#{env}"
end
end

def heroku
if options[:skip_heroku].nil?
return unless yes? "Do you use Heroku for deployment?"
elsif options[:skip_heroku]
return
end

template "Procfile"
inside("bin") { template "heroku-web" }

say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 https://docs.anycable.io/#/deployment/heroku", :yellow
end

def finish
say_status :info, "✅ AnyCable has been configured successfully!"
end

private

def install_for_docker
say_status :help, "️️⚠️ Docker development configuration could vary", :yellow

say "Here is an example snippet for docker-compose.yml:"
say <<~YML
─────────────────────────────────────────
anycable-ws:
image: anycable/anycable-go:v0.6.4
ports:
- '3334:3334'
environment:
PORT: 3334
REDIS_URL: redis://redis:6379/0
ANYCABLE_RPC_HOST: anycable-rpc:50051
depends_on:
- anycable-rpc
- redis

anycable-rpc:
<<: *backend
command: bundle exec anycable
ports:
- '50051'
─────────────────────────────────────────
YML
end

def install_for_local
install_server
template_proc_files
end

def install_server
answer = SERVER_SOURCES.index(options[:source]) || 99

until SERVER_SOURCES[answer.to_i]
answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip"
end

case answer.to_i
when 0
say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
return
else
return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
end

say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed"
end

def template_proc_files
file_name = "Procfile.dev"

if File.exist?(file_name)
append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
else
say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow

if options[:skip_procfile_dev].nil?
return unless yes? "Do you want to create a '#{file_name}' file?"
elsif options[:skip_procfile_dev]
return
end

template file_name
end
end

def install_from_brew
run "brew install anycable-go", abort_on_failure: true
run "anycable-go -v", abort_on_failure: true
end

def install_from_binary
out = options[:bin_path] if options[:bin_path]
out ||= "/usr/local/bin" if options[:method] # User don't want interactive mode
out ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true

os_name = OS_NAMES.find(&Gem::Platform.local.os.method(:==)) ||
options[:os] ||
ask("What is your OS name?", limited_to: OS_NAMES)

cpu_name = CPU_NAMES.find(&current_cpu.method(:==)) ||
options[:cpu] ||
ask("What is your CPU architecture?", limited_to: CPU_NAMES)

download_exe(
"https://github.com/anycable/anycable-go/releases/download/#{SERVER_VERSION}/" \
"anycable-go-#{SERVER_VERSION}-#{os_name}-#{cpu_name}",
to: out,
file_name: "anycable-go"
)

true
end

def download_exe(url, to:, file_name:)
file_path = File.join(to, file_name)

run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
run "#{file_path} -v", abort_on_failure: true
end

def sudo!(path)
sudo = ""
unless File.writable?(path)
if yes? "Path is not writable 😕. Do you have sudo privileges?"
sudo = "sudo "
else
say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
raise StandardError, "Path #{path} is not writable!"
end
end

sudo
end

def current_cpu
case Gem::Platform.local.cpu
when "x86_64", "x64"
"amd64"
when "x86_32", "x86", "i386", "i486", "i686"
"i386"
when "aarch64", "aarch64_be", /armv8/
"arm64"
when "arm", /armv7/, /armv6/
"arm"
else
"unknown"
end
end
end
end
2 changes: 2 additions & 0 deletions lib/rails/generators/anycable/setup/templates/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/heroku-web
release: bundle exec rails db:migrate
3 changes: 3 additions & 0 deletions lib/rails/generators/anycable/setup/templates/Procfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
server: bin/rails server
assets: bin/webpack-dev-server
anycable: bundle exec anycable --server-command "anycable-go --port 3334"
7 changes: 7 additions & 0 deletions lib/rails/generators/anycable/setup/templates/bin/heroku-web
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

if [ "$ANYCABLE_DEPLOYMENT" == "true" ]; then
bundle exec anycable --server-command="anycable-go"
else
bundle exec rails server -p $PORT -b 0.0.0.0
fi
30 changes: 30 additions & 0 deletions lib/rails/generators/anycable/setup/templates/config/anycable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file contains per-environment settings for AnyCable.
#
# Since AnyCable config is based on anyway_config (https://github.com/palkan/anyway_config), all AnyCable settings
# can be set or overridden through the corresponding environment variables.
# E.g., `rpc_host` is overridden by ANYCABLE_RPC_HOST, `debug` by ANYCABLE_DEBUG etc.
#
# Note that AnyCable recognizes REDIS_URL env variable for Redis pub/sub adapter. If you want to
# use another Redis instance for AnyCable, provide ANYCABLE_REDIS_URL variable.
#
# Read more about AnyCable configuration here: https://docs.anycable.io/#/ruby/configuration
#
default: &default
# Turn on/off access logs ("Started..." and "Finished...")
access_logs_disabled: false
# This is the host and the port to run AnyCable RPC server on.
# You must configure your WebSocket server to connect to it, e.g.:
# $ anycable-go --rpc-host="<rpc hostname>:50051"
rpc_host: "127.0.0.1:50051"
# Whether to enable gRPC level logging or not
log_grpc: false
# Use the same channel name for WebSocket server, e.g.:
# $ anycable-go --redis-channel="__anycable__"
redis_channel: "__anycable__"

development:
<<: *default
redis_url: "redis://localhost:6379/1"

production:
<<: *default
Loading