module Dota
  module API
    class Hero
      include Utilities::Mapped
      extend Utilities::Mapped

      getter id : Int8 | Int32, name : String
      private getter internalName : String

      def self.find(id)
        if mapping[id]?
          new(id)
        else
          raise Exception.new("Hero does not exist")
        end
      end

      def initialize(id : Int8 | Int32)
        @id = id
        map = mapping[id]
        @internalName = map[0].to_s
        @name = map[1].to_s
      end

      def image_url(type = :full)
        # Possible values for type:
        # :full - full quality horizontal portrait (256x114px, PNG)
        # :lg - large horizontal portrait (205x11px, PNG)
        # :sb - small horizontal portrait (59x33px, PNG)
        # :vert - full quality vertical portrait (234x272px, JPEG)

        "http://cdn.dota2.com/apps/dota2/images/heroes/#{@internal_name}_#{type}.#{type == :vert ? "jpg" : "png"}"
      end
    end
  end
end

Fix bug with wrong variable name in Hero.cr

module Dota
  module API
    class Hero
      include Utilities::Mapped
      extend Utilities::Mapped

      getter id : Int8 | Int32, name : String
      private getter internalName : String

      def self.find(id)
        if mapping[id]?
          new(id)
        else
          raise Exception.new("Hero does not exist")
        end
      end

      def initialize(id : Int8 | Int32)
        @id = id
        map = mapping[id]
        @internalName = map[0].to_s
        @name = map[1].to_s
      end

      def image_url(type = :full)
        # Possible values for type:
        # :full - full quality horizontal portrait (256x114px, PNG)
        # :lg - large horizontal portrait (205x11px, PNG)
        # :sb - small horizontal portrait (59x33px, PNG)
        # :vert - full quality vertical portrait (234x272px, JPEG)

        "http://cdn.dota2.com/apps/dota2/images/heroes/#{@internalName}_#{type}.#{type == :vert ? "jpg" : "png"}"
      end
    end
  end
end


module Raven
  # ```
  # require "raven/integrations/amber"
  # ```
  #
  # It's recommended to enable `Configuration#async` when using Amber.
  #
  # ```
  # Raven.configure do |config|
  #   # ...
  #   config.async = true
  #   config.current_environment = Amber.env.to_s
  # end
  # ```
  module Amber
    # Returns full URL string for `HTTP::Request`.
    def self.build_request_url(req : HTTP::Request)
      String.build do |url|
        url << ::Amber::Server.instance.scheme << "://" << req.host_with_port << req.resource
      end
    end
  end
end

require "./amber/**"

Add missing require in Amber integration

require "amber"

module Raven
  # ```
  # require "raven/integrations/amber"
  # ```
  #
  # It's recommended to enable `Configuration#async` when using Amber.
  #
  # ```
  # Raven.configure do |config|
  #   # ...
  #   config.async = true
  #   config.current_environment = Amber.env.to_s
  # end
  # ```
  module Amber
    # Returns full URL string for `HTTP::Request`.
    def self.build_request_url(req : HTTP::Request)
      String.build do |url|
        url << ::Amber::Server.instance.scheme << "://" << req.host_with_port << req.resource
      end
    end
  end
end

require "./amber/**"


require "./nekogirls-cr/*"
require "kemal"

Kemal.config.port = 8080
Kemal.config.public_folder = "./src/public/"
Kemal.config.host_binding = "localhost"

module Nekogirls
  get "/" do
    "Hello World"
  end

  get "/upload" do
    render "src/views/form.ecr"
  end

  post "/upload" do |env|
    file = env.params.files["file_to_upload"]
    filename = file.filename.as(String)
    file_ext = File.extname(filename)
      newfilename = unique_id + file_ext
      file_path = "./src/public/p/#{newfilename}"
      File.open(file_path, "w") do |f|
        IO.copy(file.tmpfile, f)
      end
      env.redirect "/p/#{newfilename}"
    end
  end

  error 403 do
    "Error with the filename"
  end

  Kemal.run
end

Fix for the 031aff... commit

require "./nekogirls-cr/*"
require "kemal"

Kemal.config.port = 8080
Kemal.config.public_folder = "./src/public/"
Kemal.config.host_binding = "localhost"

module Nekogirls
  get "/" do
    "Hello World"
  end

  get "/upload" do
    render "src/views/form.ecr"
  end

  post "/upload" do |env|
    file = env.params.files["file_to_upload"]
    filename = file.filename.as(String)
    file_ext = File.extname(filename)
      newfilename = unique_id + file_ext
      file_path = "./src/public/p/#{newfilename}"
      File.open(file_path, "w") do |f|
        IO.copy(file.tmpfile, f)
      end
      env.redirect "/p/#{newfilename}"
  end

  error 403 do
    "Error with the filename"
  end

  Kemal.run
end


require "http/client"
require "openssl/ssl/context"

require "./mappings/*"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"
    API_BASE = "https://discordapp.com/api/v6"

    def request(endpoint_key : Symbol, method : String, path : String, headers : HTTP::Headers, body : String?)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      HTTP::Client.exec(method: method, url: API_BASE + path, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    def get_gateway
      response = request(
        :get_gateway,
        "GET",
        "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end

    def get_channel(channel_id)
      response = request(
        :get_channel,
        "GET",
        "/channels/#{channel_id}",
        HTTP::Headers.new,
        nil
      )

      Channel.from_json(response.body)
    end

    def create_message(channel_id, content)
      response = request(
        :create_message,
        "POST",
        "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )

      Message.from_json(response.body)
    end
  end
end

Add type requirements to all the endpoint methods

require "http/client"
require "openssl/ssl/context"

require "./mappings/*"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"
    API_BASE = "https://discordapp.com/api/v6"

    def request(endpoint_key : Symbol, method : String, path : String, headers : HTTP::Headers, body : String?)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      HTTP::Client.exec(method: method, url: API_BASE + path, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    def get_gateway
      response = request(
        :get_gateway,
        "GET",
        "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end

    def get_channel(channel_id : UInt64)
      response = request(
        :get_channel,
        "GET",
        "/channels/#{channel_id}",
        HTTP::Headers.new,
        nil
      )

      Channel.from_json(response.body)
    end

    def create_message(channel_id : UInt64, content : String)
      response = request(
        :create_message,
        "POST",
        "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )

      Message.from_json(response.body)
    end
  end
end


module Scry
  module EnvironmentConfig
    private enum EnvVars
      CRYSTAL_CACHE_DIR
      CRYSTAL_PATH
      CRYSTAL_VERSION
      CRYSTAL_LIBRARY_PATH
      CRYSTAL_OPTS
    end

    def self.run
      initialize_from_crystal_env.each_with_index do |v, i|
        e = EnvVars.from_value(i).to_s
        ENV[e] = v
      end
    end

    private def self.initialize_from_crystal_env
      crystal_env
        .lines
        .to_a
    end

    private def self.crystal_env
      String.build do |io|
        Process.run("crystal", ["env"] + EnvVars.names, output: io)
      end
    end
  end
end

Fix compatibility with Crystal 0.34 stdlib

module Scry
  module EnvironmentConfig
    enum EnvVars
      CRYSTAL_CACHE_DIR
      CRYSTAL_PATH
      CRYSTAL_VERSION
      CRYSTAL_LIBRARY_PATH
      CRYSTAL_OPTS
    end

    def self.run
      initialize_from_crystal_env.each_with_index do |v, i|
        e = EnvVars.from_value(i).to_s
        ENV[e] = v
      end
    end

    private def self.initialize_from_crystal_env
      crystal_env
        .lines
        .to_a
    end

    private def self.crystal_env
      String.build do |io|
        Process.run("crystal", ["env"] + EnvVars.names, output: io)
      end
    end
  end
end


require "./app"

server = HTTP::Server.new("127.0.0.1", 8080, [
  LuckyWeb::HttpMethodOverrideHandler.new,
  HTTP::LogHandler.new,
  LuckyWeb::ErrorHandler.new(action: Errors::Show),
  LuckyWeb::RouteHandler.new,
  HTTP::StaticFileHandler.new("./public", false),
])

puts "Listening on http://127.0.0.1:8080..."

server.listen

Add session and flash handlers

require "./app"

server = HTTP::Server.new("127.0.0.1", 8080, [
  LuckyWeb::HttpMethodOverrideHandler.new,
  HTTP::LogHandler.new,
  LuckyWeb::SessionHandler.new,
  LuckyWeb::Flash::Handler.new,
  LuckyWeb::ErrorHandler.new(action: Errors::Show),
  LuckyWeb::RouteHandler.new,
  HTTP::StaticFileHandler.new("./public", false),
])

puts "Listening on http://127.0.0.1:8080..."

server.listen


require "spec"
require "../src/kemal/*"

include Kemal

Set to development before_each spec

require "spec"
require "../src/kemal/*"

include Kemal

Spec.before_each do
  Kemal.config.env = "development"
end


BaseEmail.configure do
  if Lucky::Env.production?
    send_grid_key = ENV.fetch("SEND_GRID_KEY")
    settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
  else
    settings.adapter = Carbon::DevAdapter.new
  end
end

Improve messaging when SEND_GRID_KEY is missing

BaseEmail.configure do
  if Lucky::Env.production?
    # If you don't need to send emails, set the adapter to DevAdapter instead:
    #
    #   settings.adapter = Carbon::DevAdapter.new
    #
    # If you do need emails, get a key from SendGrid and set an ENV variable
    send_grid_key = send_grid_key_from_env
    settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
  else
    settings.adapter = Carbon::DevAdapter.new
  end
end

private def send_grid_key_from_env
  ENV["SEND_GRID_KEY"]? || raise_missing_key_message
end

private def raise_missing_key_message
  raise "If you are sending emails, get a key from SendGrid and set the SEND_GRID_KEY environment variable. If you are not sending email, use the Carbon::DevAdapter in 'config/email.cr'"
end


require "spec"

module GraniteExample
  ADAPTERS = ["pg","mysql","sqlite"]
  @@model_classes = [] of Granite::ORM::Base.class

  extend self

  def model_classes
    @@model_classes
  end
end

require "../src/granite_orm"
require "./spec_models"

GraniteExample.model_classes.each do |model|
  model.drop_and_create
end

Disable query logging for `crystal spec`

require "spec"

module GraniteExample
  ADAPTERS = ["pg","mysql","sqlite"]
  @@model_classes = [] of Granite::ORM::Base.class

  extend self

  def model_classes
    @@model_classes
  end
end

require "../src/granite_orm"
require "./spec_models"

Granite::ORM.settings.logger = ::Logger.new(nil)

GraniteExample.model_classes.each do |model|
  model.drop_and_create
end


require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

struct StructWithMaybeSnowflake
  JSON.mapping(
    data: {type: UInt64 | Nil, converter: Discord::MaybeSnowflakeConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end

  describe Discord::MaybeSnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end

    it "converts null to nil" do
      json = %({"data":null})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq nil
    end
  end
end

Write a test for SnowflakeArrayConverter

require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

struct StructWithMaybeSnowflake
  JSON.mapping(
    data: {type: UInt64 | Nil, converter: Discord::MaybeSnowflakeConverter}
  )
end

struct StructWithSnowflakeArray
  JSON.mapping(
    data: {type: Array(UInt64), converter: Discord::SnowflakeArrayConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end

  describe Discord::MaybeSnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end

    it "converts null to nil" do
      json = %({"data":null})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq nil
    end
  end

  describe Discord::SnowflakeArrayConverter do
    it "converts an array of strings to u64s" do
      json = %({"data":["1", "2", "10000000000"]})

      obj = StructWithSnowflakeArray.from_json(json)
      obj.data.should be_a Array
      obj.data[0].should eq 1
      obj.data[1].should eq 2
      obj.data[2].should eq 10000000000
    end
  end
end


ch1 = Channel(String).new(16)

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

spawn do
  gcfile.each_line() do |line|
    ch1.send(line)
  end
  ch1.close
end

at = 0
gc = 0

while line = ch1.receive?
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when 'A', 'T'
      at += 1
      next
    when 'G', 'C'
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)
puts "GC fraction: #{gcfrac}"

gcfile.close

Clean up code and add comments


module Discordcr
  module REST
  end
end

Implement a very basic request method

require "http/client"

module Discordcr
  module REST
    def request(endpoint_key : Symbol, method : String, url: String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: true)
    end
  end
end


require "./app"
require "colorize"

server = HTTP::Server.new("127.0.0.1", 8080, [
  HTTP::ErrorHandler.new,
  HTTP::LogHandler.new,
  LuckyWeb::RouteHandler.new,
  HTTP::StaticFileHandler.new("./public", false),
])

puts "Listening on http://127.0.0.1:8080..."

server.listen

Handle PUT and DELETE methods

require "./app"

server = HTTP::Server.new("127.0.0.1", 8080, [
  LuckyWeb::HttpMethodOverrideHandler.new,
  HTTP::ErrorHandler.new,
  HTTP::LogHandler.new,
  LuckyWeb::RouteHandler.new,
  HTTP::StaticFileHandler.new("./public", false),
])

puts "Listening on http://127.0.0.1:8080..."

server.listen


require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      # TODO: Investigate failed nil assertion with body_io
      GatewayResponse.from_json(response.body)
    end
  end
end

Add a REST method to send a message

require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"
    POST = "POST"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      # TODO: Investigate failed nil assertion with body_io
      GatewayResponse.from_json(response.body)
    end

    def send_message(channel_id, content)
      response = request(
        :send_message,
        POST,
        API_BASE + "/channels/#{channel_id}/messages",
        HTTP::Headers{ "Content-Type" => "application/json" },
        { content: content }.to_json
      )
    end
  end
end


require "./sdl.cr/src/sdl"
require "./sdl.cr/src/image"

SDL.init(SDL::Init::VIDEO)
at_exit { SDL.quit }
window = SDL::Window.new("Hello, World!", 620, 387)

SDL::IMG.init(SDL::IMG::Init::PNG)
at_exit { SDL::IMG.quit }

png = SDL::IMG.load(File.join(__DIR__, "..", "img", "grumpy-cat.png"))
png = png.convert(window.surface)

start_time = Time.now
loop do
  png.blit(window.surface)
  window.update

  if (Time.now - start_time).seconds > 2
    break
  end
end

Update the Crystal example for Crystal 0.31.0

require "./sdl.cr/src/sdl"
require "./sdl.cr/src/image"

SDL.init(SDL::Init::VIDEO)
at_exit { SDL.quit }
window = SDL::Window.new("Hello, World!", 620, 387)

SDL::IMG.init(SDL::IMG::Init::PNG)
at_exit { SDL::IMG.quit }

png = SDL::IMG.load(File.join(__DIR__, "..", "img", "grumpy-cat.png"))
png = png.convert(window.surface)

start_time = Time.utc
loop do
  png.blit(window.surface)
  window.update

  if (Time.utc - start_time).seconds > 2
    break
  end
end


module Crow
  module Basic
    private def transpile(content : String)
      content
    end

    private def transpile(node : Crystal::NilLiteral)
      "undefined"
    end

    private def transpile(node : Crystal::ASTNode)
      transpile node.to_s
    end

    private def transpile(node : Crystal::Assign)
      "#{transpile node.target} = #{transpile node.value};"
    end
  end
end

Remove redundant transpile(content : String)

module Crow
  module Basic
    private def transpile(node : Crystal::ASTNode | String)
      node.to_s
    end

    private def transpile(node : Crystal::NilLiteral)
      "undefined"
    end

    private def transpile(node : Crystal::Assign)
      "#{transpile node.target} = #{transpile node.value};"
    end
  end
end


module Kemal
  # Kemal::InitHandler is the first handler thus initializes the context with default values such as
  # Content-Type, X-Powered-By.
  class InitHandler < HTTP::Handler
    INSTANCE = new

    def call(context)
      context.response.content_type = "text/html"
      context.response.headers.add "X-Powered-By", "Kemal"
      call_next context
    end
  end
end

Set Content-Type after call_next in Kemal::InitHandler

module Kemal
  # Kemal::InitHandler is the first handler thus initializes the context with default values such as
  # Content-Type, X-Powered-By.
  class InitHandler < HTTP::Handler
    INSTANCE = new

    def call(context)
      context.response.headers.add "X-Powered-By", "Kemal"
      call_next context
      context.response.content_type = "text/html"
    end
  end
end


require "http"

module Raven
  module Kemal
    # Exception handler capturing all unhandled `Exception`s.
    # After capturing exception is re-raised.
    #
    # ```
    # Kemal.config.add_handler(Raven::Kemal::ExceptionHandler.new)
    # # ...
    # Kemal.run
    # ```
    class ExceptionHandler
      include HTTP::Handler

      CAPTURE_DATA_FOR_METHODS = %w(POST PUT PATCH)

      private def headers_to_hash(headers : HTTP::Headers)
        headers.each_with_object(AnyHash::JSON.new) do |(k, v), hash|
          hash[k] = v.size == 1 ? v.first : v
        end
      end

      def call(context)
        call_next context
      rescue ex
        Raven.capture(ex) do |event|
          request = context.request
          if CAPTURE_DATA_FOR_METHODS.includes? request.method
            params = context.params
            data = AnyHash::JSON.new.merge! params.body.to_h, params.json
          end
          event.logger ||= "kemal"
          event.interface :http, {
            headers:      headers_to_hash(request.headers),
            method:       request.method.upcase,
            url:          Kemal.build_request_url(request),
            query_string: request.query,
            data:         data,
          }
        end
        # Raven.annotate_exception exception, ...
        # pp ex
        raise ex
      end
    end
  end
end

Send username provided by kemalcr/kemal-basic-auth

require "http"

module Raven
  module Kemal
    # Exception handler capturing all unhandled `Exception`s.
    # After capturing exception is re-raised.
    #
    # ```
    # Kemal.config.add_handler(Raven::Kemal::ExceptionHandler.new)
    # # ...
    # Kemal.run
    # ```
    class ExceptionHandler
      include HTTP::Handler

      CAPTURE_DATA_FOR_METHODS = %w(POST PUT PATCH)

      private def headers_to_hash(headers : HTTP::Headers)
        headers.each_with_object(AnyHash::JSON.new) do |(k, v), hash|
          hash[k] = v.size == 1 ? v.first : v
        end
      end

      def call(context)
        call_next context
      rescue ex
        Raven.capture(ex) do |event|
          request = context.request
          if CAPTURE_DATA_FOR_METHODS.includes? request.method
            params = context.params
            data = AnyHash::JSON.new.merge! params.body.to_h, params.json
          end
          event.logger ||= "kemal"
          event.interface :http, {
            headers:      headers_to_hash(request.headers),
            method:       request.method.upcase,
            url:          Kemal.build_request_url(request),
            query_string: request.query,
            data:         data,
          }
          if context.responds_to?(:kemal_authorized_username?)
            event.user[:username] ||= context.kemal_authorized_username?
          end
        end
        # Raven.annotate_exception exception, ...
        # pp ex
        raise ex
      end
    end
  end
end


class LuckyCli::Generators::AssetCompiler
  include LuckyCli::GeneratorHelpers

  def initialize(@project_name : String)
    @project_dir = File.join(@project_name)
    @template_dir = File.join(__DIR__, "templates")
  end

  def self.run(project_name : String)
    puts "Adding brunch config and static asset directories"
    new(project_name).install
  end

  def install
    copy_all_templates from: "static", to: "static"
    copy_all_templates from: "public", to: "public"
    copy_template from: "root/.babelrc", to: ""
    copy_template from: "root/brunch-config.js", to: ""
    copy_template from: "root/.postcssrc.yml", to: ""
    copy_template from: "root/bs-config.js", to: ""
    add_yarn_deps
  end

  private def add_yarn_deps
    puts "Installing all JavaScript dependencies"
    run_command(
      "yarn add webpack #{yarn_packages}"
    )
  end

  private def yarn_packages
    %w[
      jquery
      normalize.css
      path-complete-extname
      postcss-sass-color-functions
      rails-ujs
      turbolinks
      babel-brunch
      babel-plugin-transform-class-properties
      babel-preset-env
      browser-sync
      brunch
      fingerprinter-brunch
      glob
      lost
      postcss-brunch
      postcss-cssnext
      precss
    ].join(" ")
  end
end

Remove accidentally included webpack dep

class LuckyCli::Generators::AssetCompiler
  include LuckyCli::GeneratorHelpers

  def initialize(@project_name : String)
    @project_dir = File.join(@project_name)
    @template_dir = File.join(__DIR__, "templates")
  end

  def self.run(project_name : String)
    puts "Adding brunch config and static asset directories"
    new(project_name).install
  end

  def install
    copy_all_templates from: "static", to: "static"
    copy_all_templates from: "public", to: "public"
    copy_template from: "root/.babelrc", to: ""
    copy_template from: "root/brunch-config.js", to: ""
    copy_template from: "root/.postcssrc.yml", to: ""
    copy_template from: "root/bs-config.js", to: ""
    add_yarn_deps
  end

  private def add_yarn_deps
    puts "Installing all JavaScript dependencies"
    run_command(
      "yarn add #{yarn_packages}"
    )
  end

  private def yarn_packages
    %w[
      jquery
      normalize.css
      path-complete-extname
      postcss-sass-color-functions
      rails-ujs
      turbolinks
      babel-brunch
      babel-plugin-transform-class-properties
      babel-preset-env
      browser-sync
      brunch
      fingerprinter-brunch
      glob
      lost
      postcss-brunch
      postcss-cssnext
      precss
    ].join(" ")
  end
end


struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end
  def -(other : Int)
    self - other.to_i64
  end
  def ==(other : Int)
    self == other.to_i64
  end
  def ==(other : Pointer(_))
    self == other
  end
  def [](offset : Int) : T
    (self + offset).value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
  def unwrap : T
    self[0]
  end
  def null?
    self == 0_u64
  end
end

Add <, > and to_byte_ptr functions to Pointer

struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end
  def -(other : Int)
    self - other.to_i64
  end
  def <(other : Pointer(_))
    self < other
  end
  def >(other : Pointer(_))
    self > other
  end
  def ==(other : Int)
    self == other.to_i64
  end
  def ==(other : Pointer(_))
    self == other
  end
  def [](offset : Int) : T
    (self + offset).value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
  def unwrap : T
    self[0]
  end
  def null?
    self == 0_u64
  end
  def to_byte_ptr
    self.as(Pointer(UInt8))
  end
end


require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

Add a nilable email field to the User mapping (for OAuth2)

require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String,
      email: String?
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end


class Params
	getter :hash

	def initialize(default="")
		@params = Hash(String, String).new(default)
	end

	def hash
		@params
	end

	def merge(other : Params)
		merged_hash = @params.merge other.hash
		merged_params = Params.new
		merged_params.from_hash(merged_hash)
		merged_params
	end

	def [](key)
		@params[key.to_s]
	end

	def from_hash(hash : Hash)
	  hash.each { |k,v| @params[k.to_s] = v.to_s }
	end

	def []=(k,v)
		@params[k.to_s] = v.to_s
	end

	def ==(other : Hash)
		other == @params
	end

	def inspect
		@params.inspect
	end

	def has_keys?(keys_array : Array)
		has = true
		keys_array.each do |key|
			has = false unless @params.has_key? key.to_s
		end
		has
	end

	forward_missing_to @params
end

Add to_s method for Params

class Params
	getter :hash

	def initialize(default="")
		@params = Hash(String, String).new(default)
	end

	def hash
		@params
	end

	def merge(other : Params)
		merged_hash = @params.merge other.hash
		merged_params = Params.new
		merged_params.from_hash(merged_hash)
		merged_params
	end

	def [](key)
		@params[key.to_s]
	end

	def from_hash(hash : Hash)
	  hash.each { |k,v| @params[k.to_s] = v.to_s }
	end

	def []=(k,v)
		@params[k.to_s] = v.to_s
	end

	def ==(other : Hash)
		other == @params
	end

	def inspect
		@params.inspect
	end

	def has_keys?(keys_array : Array)
		has = true
		keys_array.each do |key|
			has = false unless @params.has_key? key.to_s
		end
		has
	end

  def to_s(io : IO)
    msg = "\n{"
    @params.each do |k,v|
      msg+="#{k} => #{v}"
    end
    io << msg
  end

	forward_missing_to @params
end


require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body_io)
    end
  end
end

Use `response.body` instead of `body_io` as the JSON source

require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end
  end
end


struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end
  def -(other : Int)
    self - other.to_i64
  end
  def <(other : Pointer(_))
    self < other
  end
  def >(other : Pointer(_))
    self > other
  end
  def ==(other : Int)
    self == other.to_i64
  end
  def ==(other : Pointer(_))
    self == other
  end
  def [](offset : Int) : T
    (self + offset).value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
  def unwrap : T
    self[0]
  end
  def null?
    self == 0_u64
  end
  def to_byte_ptr
    self.as(Pointer(UInt8))
  end
end

Fix Pointer(T).null? and add Pointer(T).null

struct Pointer(T)
  def self.null
    new(0_u64)
  end
  def +(other : Int)
    self + other.to_i64
  end
  def -(other : Int)
    self - other.to_i64
  end
  def <(other : Pointer(_))
    self < other
  end
  def >(other : Pointer(_))
    self > other
  end
  def ==(other : Int)
    self == other.to_i64
  end
  def ==(other : Pointer(_))
    self == other
  end
  def [](offset : Int) : T
    (self + offset).value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
  def null?
    address == 0
  end
  def to_byte_ptr
    self.as(Pointer(UInt8))
  end
end


#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  # The Kemal app, pretty straightforward.

  # For root and /app.js we just hardcode some paths. You should probably use a nicer way for this.
  get "/" do |ctx|
    send_file ctx, "src/web/index.html"
  end

  get "/app.js" do |ctx|
    send_file ctx, "src/web/app.js"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us, but leaving this in as an
      # example for how you could do stuff when people send you messages
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  Kemal.run
end

Change endpoints in the app to use the /public directory

#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket # which type are the objects in this empty list :O

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  # The Kemal app, pretty straightforward.

  # While static files get served from the /public directory, this doesn't work for calls to `/`, so
  # we have to hardcode that one.
  get "/" do |ctx|
    send_file ctx, "public/index.html"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us, but leaving this in as an
      # example for how you could do stuff when people send you messages
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  Kemal.run
end


require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String,
      email: String?
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

Add a nilable bot field to User (for bot accounts)

require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String,
      email: String?,
      bot: Bool?
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end


require "./converters"

module Discord
  # :nodoc:
  def initialize(partial : PartialUser)
    @username = partial.username.not_nil!
    @id = partial.id
    @discriminator = partial.discriminator.not_nil!
    @avatar = partial.avatar
    @email = partial.email
    @bot = partial.bot
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )

    def full? : Bool
      !@username.nil? && !@discriminator.nil? && !@avatar.nil?
    end
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

Make sure to put the User initializer at the correct place...

require "./converters"

module Discord
  struct User
    # :nodoc:
    def initialize(partial : PartialUser)
      @username = partial.username.not_nil!
      @id = partial.id
      @discriminator = partial.discriminator.not_nil!
      @avatar = partial.avatar
      @email = partial.email
      @bot = partial.bot
    end

    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )

    def full? : Bool
      !@username.nil? && !@discriminator.nil? && !@avatar.nil?
    end
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end


require "./nekogirls-cr/*"
require "kemal"

Kemal.config.port = 8080
Kemal.config.public_folder = "./src/public/"

module Nekogirls
  get "/" do
    "Hello World"
  end

  get "/upload" do
    render "src/views/form.ecr"
  end

  post "/upload" do |env|
    file = env.params.files["file_to_upload"]
    filename = file.filename.as(String)
    file_ext = File.extname(filename)
    # Be sure to check if file.filename is not empty otherwise it'll raise a compile time error
    if !filename.is_a?(String)
      p "No filename included in upload"
    else
      newfilename = unique_id + file_ext
      file_path = "./src/public/p/#{newfilename}"
      File.open(file_path, "w") do |f|
        IO.copy(file.tmpfile, f)
      end
      env.redirect "/p/#{newfilename}"
    end
  end

  Kemal.run
end

Return HTTP error 400 if the filename is broken

require "./nekogirls-cr/*"
require "kemal"

Kemal.config.port = 8080
Kemal.config.public_folder = "./src/public/"

module Nekogirls
  get "/" do
    "Hello World"
  end

  get "/upload" do
    render "src/views/form.ecr"
  end

  post "/upload" do |env|
    file = env.params.files["file_to_upload"]
    filename = file.filename.as(String)
    file_ext = File.extname(filename)
    # Be sure to check if file.filename is not empty otherwise it'll raise a compile time error
    if !filename.is_a?(String)
      env.response.status_code = 400
    else
      newfilename = unique_id + file_ext
      file_path = "./src/public/p/#{newfilename}"
      File.open(file_path, "w") do |f|
        IO.copy(file.tmpfile, f)
      end
      env.redirect "/p/#{newfilename}"
    end
  end

  error 403 do
    "Error with the filename"
  end

  Kemal.run
end


require "spec2"
require "../src/m3u8"

include Spec2::GlobalDSL

Use spec2 doc reporter for specs

require "spec2"
require "../src/m3u8"

include Spec2::GlobalDSL

Spec2.configure_reporter(Spec2::Reporters::Doc)


module Soil
  module Http
    module Handlers
      class MainHandler
        include HTTP::Handler

        def initialize(app_class : App.class)
          @app_class = app_class
        end

        def call(raw_context)
          context = build_context(raw_context)
          @app_class
            .find(context.request)
            .call(context.request, context.response)

          call_next(raw_context) unless context.response.halted?
        end

        private def build_context(raw_context)
          Context.new(raw_context, @app_class)
        end
      end
    end
  end
end

Check if next handler is present before calling it or a 404 will be triggered by Crystal

module Soil
  module Http
    module Handlers
      class MainHandler
        include HTTP::Handler

        def initialize(app_class : App.class)
          @app_class = app_class
        end

        def call(raw_context)
          context = build_context(raw_context)

          @app_class
            .find(context.request)
            .call(context.request, context.response)

          if has_next_handler? && !context.response.halted?
            call_next(raw_context)
          end
        end

        private def build_context(raw_context)
          Context.new(raw_context, @app_class)
        end

        private def has_next_handler?
          !!@next
        end
      end
    end
  end
end


require "./base"

module Liquid::Filters
  #   strip_newlines
  #
  # Removes any newline characters (line breaks) from a string.
  #
  # Input
  # {% capture string_with_newlines %}
  # Hello
  # there
  # {% endcapture %}
  #
  # {{ string_with_newlines | newline_to_br }}
  #
  # Output
  # Hellothere
  class StripNewLines
    extend Filter

    def self.filter(data : Any, args : Array(Any)? = nil) : Any
      if data.raw.responds_to? :to_s
        Any.new data.raw.to_s.gsub /\r?\n/, ""
      else
        data
      end
    end
  end

  FilterRegister.register "strip_newlines", StripNewLines
end

Fix wrong filter name in filter documentation

require "./base"

module Liquid::Filters
  #   strip_newlines
  #
  # Removes any newline characters (line breaks) from a string.
  #
  # Input
  # {% capture string_with_newlines %}
  # Hello
  # there
  # {% endcapture %}
  #
  # {{ string_with_newlines | strip_newlines }}
  #
  # Output
  # Hellothere
  class StripNewLines
    extend Filter

    def self.filter(data : Any, args : Array(Any)? = nil) : Any
      if data.raw.responds_to? :to_s
        Any.new data.raw.to_s.gsub /\r?\n/, ""
      else
        data
      end
    end
  end

  FilterRegister.register "strip_newlines", StripNewLines
end


require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end
  end
end

Add a TODO to remind me of the body_io problem

require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      # TODO: Investigate failed nil assertion with body_io
      GatewayResponse.from_json(response.body)
    end
  end
end


require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      else
        puts "Unsupported message: #{message}"
      end
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.milliseconds
        end
      end
    end
  end
end

Make sure to convert the heartbeat_interval to an int

require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      else
        puts "Unsupported message: #{message}"
      end
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end
  end
end


# This is used when generating URLs for your application
Lucky::RouteHelper.configure do |settings|
  if Lucky::Env.production?
    # Example: https://my_app.com
    settings.base_uri = ENV.fetch("APP_DOMAIN")
  else
    # Set domain to the default host/port in development/test
    settings.base_uri = "http://#{Lucky::ServerSettings.host}:#{Lucky::ServerSettings.port}"
  end
end

Use localhost for the RouteHelper

# This is used when generating URLs for your application
Lucky::RouteHelper.configure do |settings|
  if Lucky::Env.production?
    # Example: https://my_app.com
    settings.base_uri = ENV.fetch("APP_DOMAIN")
  else
    # Set domain to the default host/port in development/test
    settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}"
  end
end


require "./TreeVisitor.cr"

module Charly::AST

  # Dump a human-readable version of the AST
  class DumpVisitor < TreeVisitor

    # Catch all rule
    visit ASTNode do
      io.puts name node
      rest children
    end

    visit StringLiteral do
      io << name node
      io << " | "
      io.puts "\"#{node.value}\""
    end

    visit PrecalculatedValue, NumericLiteral, BooleanLiteral do
      io << name node
      io << " | "
      io.puts node.value
    end

    visit IdentifierLiteral, FunctionLiteral, ClassLiteral, PrimitiveClassLiteral do
      io << name node
      io << " | "
      io.puts "\"#{node.name}\""
      rest children
    end

    visit UnaryExpression, BinaryExpression, ComparisonExpression do
      io << name node
      io << " | "
      io.puts node.operator
      rest children
    end

    macro rest(children)
      {{children}}.each_with_index do |child, index|

        unless child.is_a? ASTNode
          next
        end

        str = String.build do |str|
          child.accept self, str
        end

        str.lines.each_with_index do |line, line_index|

          if line_index == 0
            if {{children}}.size > 1 && index < {{children}}.size - 1
              io << "├─"
            else
              io << "└─"
            end
          else
            if {{children}}.size > 1 && index < {{children}}.size - 1
              io << "│ "
            else
              io << "  "
            end
          end

          io << " "
          io << line

          io << "\n"
        end
      end
    end

  end

end

Append newline after nodes with children

require "./TreeVisitor.cr"

module Charly::AST

  # Dump a human-readable version of the AST
  class DumpVisitor < TreeVisitor

    # Catch all rule
    visit ASTNode do
      io.puts name node
      rest children
    end

    visit StringLiteral do
      io << name node
      io << " | "
      io.puts "\"#{node.value}\""
    end

    visit PrecalculatedValue, NumericLiteral, BooleanLiteral do
      io << name node
      io << " | "
      io.puts node.value
    end

    visit IdentifierLiteral, FunctionLiteral, ClassLiteral, PrimitiveClassLiteral do
      io << name node
      io << " | "
      io.puts "\"#{node.name}\""
      rest children
    end

    visit UnaryExpression, BinaryExpression, ComparisonExpression do
      io << name node
      io << " | "
      io.puts node.operator
      rest children
    end

    macro rest(children)
      {{children}}.each_with_index do |child, index|

        unless child.is_a? ASTNode
          next
        end

        str = String.build do |str|
          child.accept self, str
        end

        str.lines.each_with_index do |line, line_index|

          if line_index == 0
            if {{children}}.size > 1 && index < {{children}}.size - 1
              io << "├─"
            else
              io << "└─"
            end
          else
            if {{children}}.size > 1 && index < {{children}}.size - 1
              io << "│ "
            else
              io << "  "
            end
          end

          io << " "
          io << line

          io << "\n"
        end
      end

      io << "\n"
    end

  end

end


require "./parser"

# :nodoc:
class Crustache::IndentIO < IO

  def initialize(@indent : String, @io : IO)
    @indent_flag = 0
    @eol_flag = true
  end

  def indent_flag_on
    @indent_flag -= 1
  end

  def indent_flag_off
    @indent_flag += 1
  end

  def read(s : Slice(UInt8))
    raise "Unsupported"
  end

  def write(s) : Nil
    start = 0
    size = s.size
    i = 0
    while i < size
      if @eol_flag
        @io.write s[start, i - start]
        @io << @indent
        @eol_flag = false
        start = i
      end

      if s[i] == Parser::NEWLINE_N && @indent_flag == 0
        @eol_flag = true
      end

      i += 1
    end

    @io.write s[start, i - start]
  end
end

Fix return type restriction since Crystal 0.35.0

require "./parser"

# :nodoc:
class Crustache::IndentIO < IO
  def initialize(@indent : String, @io : IO)
    @indent_flag = 0
    @eol_flag = true
  end

  def indent_flag_on
    @indent_flag -= 1
  end

  def indent_flag_off
    @indent_flag += 1
  end

  def read(s : Slice(UInt8))
    raise "Unsupported"
  end

  {% begin %}
    def write(s) : {% if compare_versions(Crystal::VERSION, "0.35.0") >= 0 %}Int64{% else %}Nil{% end %}
      start = 0
      size = s.size
      i = 0
      while i < size
        if @eol_flag
          @io.write s[start, i - start]
          @io << @indent
          @eol_flag = false
          start = i
        end

        if s[i] == Parser::NEWLINE_N && @indent_flag == 0
          @eol_flag = true
        end

        i += 1
      end

      @io.write s[start, i - start]
    end
  {% end %}
end


require "logger"

class Logger
  private LOGGER_BREADCRUMB_LEVELS = {
    Severity::DEBUG => Raven::Breadcrumb::Severity::DEBUG,
    Severity::INFO  => Raven::Breadcrumb::Severity::INFO,
    Severity::WARN  => Raven::Breadcrumb::Severity::WARNING,
    Severity::ERROR => Raven::Breadcrumb::Severity::ERROR,
    Severity::FATAL => Raven::Breadcrumb::Severity::CRITICAL,
  }

  protected def self.ignored_logger?(progname)
    Raven.configuration.exclude_loggers.includes?(progname)
  end

  protected def self.deansify(message)
    message.gsub(/\x1b[^m]*m/, "")
  end

  private def write(severity, datetime, progname, message)
    unless self.class.ignored_logger?(progname)
      Raven.breadcrumbs.record do |crumb|
        crumb.timestamp = datetime
        crumb.level = LOGGER_BREADCRUMB_LEVELS[severity]?
        crumb.category = progname || "logger"
        crumb.message = self.class.deansify(message)
      end
    end
    previous_def
  end
end

Fix `Logger.deansify` not accepting types other than String

require "logger"

class Logger
  private LOGGER_BREADCRUMB_LEVELS = {
    Severity::DEBUG => Raven::Breadcrumb::Severity::DEBUG,
    Severity::INFO  => Raven::Breadcrumb::Severity::INFO,
    Severity::WARN  => Raven::Breadcrumb::Severity::WARNING,
    Severity::ERROR => Raven::Breadcrumb::Severity::ERROR,
    Severity::FATAL => Raven::Breadcrumb::Severity::CRITICAL,
  }

  protected def self.ignored_logger?(progname)
    Raven.configuration.exclude_loggers.includes?(progname)
  end

  protected def self.deansify(message)
    case message
    when Nil       then nil
    when String    then message.gsub(/\x1b[^m]*m/, "")
    when Exception then deansify(message.message)
    else                deansify(message.to_s)
    end
  end

  private def write(severity, datetime, progname, message)
    unless self.class.ignored_logger?(progname)
      Raven.breadcrumbs.record do |crumb|
        crumb.timestamp = datetime
        crumb.level = LOGGER_BREADCRUMB_LEVELS[severity]?
        crumb.category = progname || "logger"
        crumb.message = self.class.deansify(message)
      end
    end
    previous_def
  end
end


module Discordcr
  class Client
    def initialize(@token : String, @client_id : UInt64)
    end

    def run
    end
  end
end

Include the REST module in Client

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
    end
  end
end


require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping (
        url: String
      )
    end
  end
end

Fix an errant space in the mapping definition

require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end
end


module Raven
  class Processor::RemoveCircularReferences < Processor
    def process(data, visited = [] of UInt64)
      return data unless data.responds_to? :object_id
      return "(...)" if visited.includes? data.object_id

      case data
      when AnyHash::JSON
        visited << data.to_h.object_id
        data.each do |k, v|
          data[k] = process(v, visited) rescue "!"
        end
        data.to_h
      when Hash
        process data.to_any_json, visited
      when Array
        data.map! { |v| process v, visited }
      else
        data
      end
    end
  end
end

Fix RemoveCircularReferences to play nice with types

module Raven
  class Processor::RemoveCircularReferences < Processor
    def process(data, visited = [] of UInt64)
      return data unless data.responds_to? :object_id
      return "(...)" if visited.includes? data.object_id

      case data
      when AnyHash::JSON
        visited << data.to_h.object_id
        data.each do |k, v|
          data[k] = process(v, visited) rescue "!"
        end
        data.to_h
      when Hash
        process data.to_any_json, visited
      when Array
        data.map { |v| process(v, visited).as(typeof(v)) }
      else
        data
      end
    end
  end
end


module Amethyst
  module Exceptions
    class HttpException < AmethystException
      getter :status
      getter :msg

      def initialize(@status, @msg)
        super()
      end
    end

    class UnknownContentType < AmethystException

      def initialize(@ext)
        super("Unknown content-type for file extension #{@ext}")
      end
    end

    class UnsupportedHttpMethod < AmethystException

      def initialize(@ext)
        super("Method #{@method} is not supported. Use #{Http::METHODS.join(" ")}")
      end
    end

    class HttpNotFound < HttpException

      def initialize
        super(404, "Not found")
      end
    end

    class HttpBadRequest < HttpException

      def initialize
        super(400, "Bad request")
      end
    end

    class HttpMethodNotAllowed < HttpException
      getter :method
      getter :allowed

      def initialize(@method, allowed : Array)
        @allowed = allowed.join(",")
        super(405, "Method #{method} not allowed. Allowed : #{@allowed}")
      end
    end

    class HttpNotImplemented < HttpException
      getter :method

      def initialize(@method)
        super(501, "Method #{@method} not implemented")
      end
    end
  end
end

Fix wrong argument usage in HTTPExceptions

module Amethyst
  module Exceptions
    class HttpException < AmethystException
      getter :status
      getter :msg

      def initialize(@status, @msg)
        super()
      end
    end

    class UnknownContentType < AmethystException

      def initialize(@ext : String)
        super("Unknown content-type for file extension #{@ext}")
      end
    end

    class UnsupportedHttpMethod < AmethystException

      def initialize(@ext : String)
        super("Method #{@ext} is not supported. Use #{Http::METHODS.join(" ")}")
      end
    end

    class HttpNotFound < HttpException

      def initialize
        super(404, "Not found")
      end
    end

    class HttpBadRequest < HttpException

      def initialize
        super(400, "Bad request")
      end
    end

    class HttpMethodNotAllowed < HttpException
      getter :method
      getter :allowed

      def initialize(@method : String, allowed : Array)
        @allowed = allowed.join(",")
        super(405, "Method #{@method} not allowed. Allowed : #{@allowed}")
      end
    end

    class HttpNotImplemented < HttpException
      getter :method

      def initialize(@method : String)
        super(501, "Method #{@method} not implemented")
      end
    end
  end
end


class Shared::LayoutHead < BaseComponent
  needs page_title : String
  needs context : HTTP::Server::Context

  def render
    head do
      utf8_charset
      title "My App - #{@page_title}"
      css_link asset("css/app.css"), data_turbolinks_track: "reload"
      js_link asset("js/app.js"), defer: "true", data_turbolinks_track: "reload"
      csrf_meta_tags
      responsive_meta_tag
    end
  end
end

Disable turbolinks cache by default

class Shared::LayoutHead < BaseComponent
  needs page_title : String
  needs context : HTTP::Server::Context

  def render
    head do
      utf8_charset
      title "My App - #{@page_title}"
      css_link asset("css/app.css"), data_turbolinks_track: "reload"
      js_link asset("js/app.js"), defer: "true", data_turbolinks_track: "reload"
      meta name: "turbolinks-cache-control", content: "no-cache"
      csrf_meta_tags
      responsive_meta_tag
    end
  end
end


require "./franklin"
require "option_parser"

config_path = Franklin::Config::DEFAULT_FILE_LOCATION
filter_type = nil

parser = OptionParser.parse! { |parser|
  parser.banner = "Usage: frankling [options] search_term1 search_term2..."

  parser.on("-h", "--help", "Prints this help") do
    puts parser
    puts "Franklin v#{Franklin::VERSION}"
    exit
  end

  parser.on("--config-path=CONFIG", "Path to configuration file. Defaults to ~/.franklin") do |path|
    config_path = path
  end

  parser.on "-tTYPE", "--type=TYPE", "Filter results by type (eBook, Audiobook, etc). Will override config" do |type|
    filter_type = type
  end
}

begin
  Franklin.run(search_terms: ARGV.join(" "), config_path: config_path, filter: filter_type)
rescue ex : ArgumentError
  STDERR.puts <<-ERROR
  Oops! #{ex.message}
  #{parser}
  ERROR
rescue Errno
  STDERR.puts <<-ERROR
  Oops! It looks like you have not configured franklin
  Please add a configuration file in $HOME/.franklin that looks like this:
  ---
  libraries:
    - name: San Francisco Public Library
      url: http://sfpl.lib.overdrive.com
    - name: San Diego Public Library
      url: http://sdpl.lib.overdrive.com
  default_type: eBook # Optional, leave empty to search for all types
  ERROR
end

Add version information on errors

require "./franklin"
require "option_parser"

config_path = Franklin::Config::DEFAULT_FILE_LOCATION
filter_type = nil

parser = OptionParser.parse! { |parser|
  parser.banner = "Usage: frankling [options] search_term1 search_term2..."

  parser.on("-h", "--help", "Prints this help") do
    puts parser
    puts "Franklin v#{Franklin::VERSION}"
    exit
  end

  parser.on("--config-path=CONFIG", "Path to configuration file. Defaults to ~/.franklin") do |path|
    config_path = path
  end

  parser.on "-tTYPE", "--type=TYPE", "Filter results by type (eBook, Audiobook, etc). Will override config" do |type|
    filter_type = type
  end
}

begin
  Franklin.run(search_terms: ARGV.join(" "), config_path: config_path, filter: filter_type)
rescue ex : ArgumentError
  STDERR.puts <<-ERROR
  Oops! #{ex.message}
  #{parser}
  Franklin v#{Franklin::VERSION}
  ERROR
rescue Errno
  STDERR.puts <<-ERROR
  Oops! It looks like you have not configured franklin
  Please add a configuration file in $HOME/.franklin that looks like this:
  ---
  libraries:
    - name: San Francisco Public Library
      url: http://sfpl.lib.overdrive.com
    - name: San Diego Public Library
      url: http://sdpl.lib.overdrive.com
  default_type: eBook # Optional, leave empty to search for all types
  ERROR
end


abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User
  needs flash : Lucky::Flash::Store

  abstract def inner

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
      end

      body do
        render_flash
        inner
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end

Add CSRF meta tags to layout

abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User
  needs flash : Lucky::Flash::Store

  abstract def inner

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
        csrf_meta_tags
      end

      body do
        render_flash
        inner
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end


require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end
end

Make the type field possibly be nil (apparently it is sometimes)

require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8 | Nil,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end
end


require "logger"

# Logger #######################################################################

LOG = Logger.new(STDOUT)
LOG.level = Logger::INFO
LOG.formatter = Logger::Formatter.new do |severity, _datetime, _progname, message, io|
  io << case severity
  when "DEBUG"
    ":: "
  when "INFO"
    ">> "
  when "WARN"
    "** "
  when "ERROR"
    "!! "
  when "FATAL"
    "   " # FIXME: Bullet for FATAL
  end
  io << message
  io << "..." if severity == "INFO"
end

# Main #########################################################################

LOG.info("Help!")

Add colours to log messages

require "logger"
require "colorize"

# Logger #######################################################################

LOG = Logger.new(STDOUT)
LOG.level = Logger::INFO
LOG.formatter = Logger::Formatter.new do |severity, _datetime, _progname, message, io|
  io << case severity
  when "DEBUG"
    ":: ".colorize.white
  when "INFO"
    ">> ".colorize.green
  when "WARN"
    "** ".colorize.yellow
  when "ERROR"
    "!! ".colorize.red
  when "FATAL"
    "   " # FIXME: Bullet for FATAL
  end
  io << message
  io << "..." if severity == "INFO"
end

# Main #########################################################################

LOG.info("Help!")


module Optarg
  # :nodoc:
  abstract class ValueHash(V) < Hash(String, V)
    @fallbacked = {} of String => Bool
    @parser : Parser

    def initialize(@parser)
      super()
    end

    def [](key)
      fallback key
      super
    end

    def []?(key)
      fallback key
      super
    end

    def fallback(key)
      return if has_key?(key)
      return if @fallbacked.has_key?(key)
      @fallbacked[key] = true
      if fb = @parser.definitions.values[key]?
        fb.fallback_value @parser
      end
    end
  end
end

Use Hash composition over inheritance in ValueHash

module Optarg
  # :nodoc:
  abstract class ValueHash(V)
    @raw = {} of String => V
    forward_missing_to @raw

    @fallbacked = {} of String => Bool
    @parser : Parser

    def initialize(@parser)
    end

    def ==(other : Hash)
      @raw == other
    end

    def [](key)
      fallback key
      @raw[key]
    end

    def []?(key)
      fallback key
      @raw[key]?
    end

    def fallback(key)
      return if @raw.has_key?(key)
      return if @fallbacked.has_key?(key)
      @fallbacked[key] = true
      if fb = @parser.definitions.values[key]?
        fb.fallback_value @parser
      end
    end
  end
end


module Discord
  @[Flags]
  enum Permissions : UInt64
    CreateInstantInvite = 1
    KickMembers         = 1 << 1
    BanMembers          = 1 << 2
    Administrator       = 1 << 3
    ManageChannels      = 1 << 4
    ManageGuild         = 1 << 5
    ReadMessages        = 1 << 10
    SendMessages        = 1 << 11
    SendTTSMessages     = 1 << 12
    ManageMessages      = 1 << 13
    EmbedLinks          = 1 << 14
    AttachFiles         = 1 << 15
    ReadMessageHistory  = 1 << 16
    MentionEveryone     = 1 << 17
    UseExternalEmojis   = 1 << 18
    Connect             = 1 << 20
    Speak               = 1 << 21
    MuteMembers         = 1 << 22
    DeafenMembers       = 1 << 23
    MoveMembers         = 1 << 24
    UseVAD              = 1 << 25
    ChangeNickname      = 1 << 26
    ManageNicknames     = 1 << 27
    ManageRoles         = 1 << 28
    ManageWebhooks      = 1 << 29
    ManageEmojis        = 1 << 30

    def self.new(pull : JSON::PullParser)
      # see https://github.com/crystal-lang/crystal/issues/3448
      # #from_value errors
      Permissions.new(pull.read_int.to_u64)
    end
  end
end

Add AddReactions value (1<<6) to Permissions enum

module Discord
  @[Flags]
  enum Permissions : UInt64
    CreateInstantInvite = 1
    KickMembers         = 1 << 1
    BanMembers          = 1 << 2
    Administrator       = 1 << 3
    ManageChannels      = 1 << 4
    ManageGuild         = 1 << 5
    AddReactions        = 1 << 6
    ReadMessages        = 1 << 10
    SendMessages        = 1 << 11
    SendTTSMessages     = 1 << 12
    ManageMessages      = 1 << 13
    EmbedLinks          = 1 << 14
    AttachFiles         = 1 << 15
    ReadMessageHistory  = 1 << 16
    MentionEveryone     = 1 << 17
    UseExternalEmojis   = 1 << 18
    Connect             = 1 << 20
    Speak               = 1 << 21
    MuteMembers         = 1 << 22
    DeafenMembers       = 1 << 23
    MoveMembers         = 1 << 24
    UseVAD              = 1 << 25
    ChangeNickname      = 1 << 26
    ManageNicknames     = 1 << 27
    ManageRoles         = 1 << 28
    ManageWebhooks      = 1 << 29
    ManageEmojis        = 1 << 30

    def self.new(pull : JSON::PullParser)
      # see https://github.com/crystal-lang/crystal/issues/3448
      # #from_value errors
      Permissions.new(pull.read_int.to_u64)
    end
  end
end


require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "get"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      puts response.body

      GatewayResponse.from_json(response.body_io)
    end
  end
end

Use the correct method for the request

require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      puts response.body

      GatewayResponse.from_json(response.body_io)
    end
  end
end


module Raven
  class Context
    # FIXME
    # @[ThreadLocal]
    @@current : self?

    def self.current
      @@current ||= new
    end

    def self.clear!
      @@current = nil
    end

    class_getter os_context : AnyHash::JSON do
      {
        name:           Raven.sys_command("uname -s"),
        version:        Raven.sys_command("uname -v"),
        build:          Raven.sys_command("uname -r"),
        kernel_version: Raven.sys_command("uname -a") || Raven.sys_command("ver"), # windows
      }.to_any_json
    end

    class_getter runtime_context : AnyHash::JSON do
      v = Raven.sys_command_compiled("crystal -v").match /^(.+?) (\d+.*)$/
      _, name, version = v.not_nil!
      {
        name:    name,
        version: version,
      }.to_any_json
    end

    any_json_property :contexts, :extra, :tags, :user

    def initialize
      @contexts = {
        os:      self.class.os_context,
        runtime: self.class.runtime_context,
      }.to_any_json
    end
  end
end

Use `Crystal::DESCRIPTION` instead of spawning `crystal -v`

module Raven
  class Context
    # FIXME
    # @[ThreadLocal]
    @@current : self?

    def self.current
      @@current ||= new
    end

    def self.clear!
      @@current = nil
    end

    class_getter os_context : AnyHash::JSON do
      {
        name:           Raven.sys_command("uname -s"),
        version:        Raven.sys_command("uname -v"),
        build:          Raven.sys_command("uname -r"),
        kernel_version: Raven.sys_command("uname -a") || Raven.sys_command("ver"), # windows
      }.to_any_json
    end

    class_getter runtime_context : AnyHash::JSON do
      v = Crystal::DESCRIPTION.match /^(.+?) (\d+.*)$/
      _, name, version = v.not_nil!
      {
        name:    name,
        version: version,
      }.to_any_json
    end

    any_json_property :contexts, :extra, :tags, :user

    def initialize
      @contexts = {
        os:      self.class.os_context,
        runtime: self.class.runtime_context,
      }.to_any_json
    end
  end
end


require "./converters"
require "./user"
require "./channel"
require "./guild"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end

    struct GuildBanPayload
      JSON.mapping(
        username: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        discriminator: String,
        avatar: String,
        bot: {type: Bool, nilable: true},
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end

    struct GuildEmojiUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter},
        emoji: {type: Array(Emoji), key: "emojis"}
      )
    end
  end
end

Add a mapping for the GUILD_INTEGRATIONS_UPDATE dispatch payload

require "./converters"
require "./user"
require "./channel"
require "./guild"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end

    struct GuildBanPayload
      JSON.mapping(
        username: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        discriminator: String,
        avatar: String,
        bot: {type: Bool, nilable: true},
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end

    struct GuildEmojiUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter},
        emoji: {type: Array(Emoji), key: "emojis"}
      )
    end

    struct GuildIntegrationsUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end
  end
end


ch1 = Channel(String).new(16)

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

spawn do
  gcfile.each_line() do |line|
    ch1.send(line)
  end
  ch1.close
end

at = 0
gc = 0

while line = ch1.receive
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when 'A', 'T'
      at += 1
      next
    when 'G', 'C'
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)
puts "GC fraction: #{gcfrac}"

gcfile.close

Fix receive on closed chan error

ch1 = Channel(String).new(16)

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

spawn do
  gcfile.each_line() do |line|
    ch1.send(line)
  end
  ch1.close
end

at = 0
gc = 0

while line = ch1.receive?
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when 'A', 'T'
      at += 1
      next
    when 'G', 'C'
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)
puts "GC fraction: #{gcfrac}"

gcfile.close


require "any_hash"

require "./raven/ext/*"
require "./raven/mixins/*"
require "./raven/*"

module Raven
  # `Raven.instance` delegators.
  module Delegators
    delegate :context, :configuration, :client,
      :report_status, :configure, :send_feedback, :send_event,
      :capture, :last_event_id, :annotate_exception,
      :user_context, :tags_context, :extra_context, :breadcrumbs,
      to: :instance
  end
end

module Raven
  extend Delegators

  class_getter instance : Raven::Instance { Raven::Instance.new }

  macro sys_command_compiled(command)
    %result = {{ `(#{command.id} || true) 2>/dev/null`.stringify.strip }}
    %result unless %result.empty?
  end

  def self.sys_command(command)
    result = `(#{command}) 2>/dev/null`.strip rescue nil
    result if !result.nil? && !result.empty? && $?.success?
  end
end

Simplify logic just a bit

require "any_hash"

require "./raven/ext/*"
require "./raven/mixins/*"
require "./raven/*"

module Raven
  # `Raven.instance` delegators.
  module Delegators
    delegate :context, :configuration, :client,
      :report_status, :configure, :send_feedback, :send_event,
      :capture, :last_event_id, :annotate_exception,
      :user_context, :tags_context, :extra_context, :breadcrumbs,
      to: :instance
  end
end

module Raven
  extend Delegators

  class_getter instance : Raven::Instance { Raven::Instance.new }

  macro sys_command_compiled(command)
    %result = {{ `(#{command.id} || true) 2>/dev/null`.stringify.strip }}
    %result.presence
  end

  def self.sys_command(command)
    result = `(#{command}) 2>/dev/null`.strip rescue nil
    result.presence if $?.success?
  end
end


abstract class LuckyCli::Task
  macro inherited
    LuckyCli::Runner.tasks << self.new

    def name
      "{{@type.name.gsub(/::/, ".").underscore}}"
    end
  end

  macro banner(banner_text)
    def banner
      {{banner_text}}
    end
  end

  # Set a custom title for the task
  #
  # By default the name is derived from the full module and class name.
  # However if that name is not desired, a custom one can be set here.
  # ```
  # class Dev::Prime < LuckyCli::Task
  #   name "Development database primer"
  #   banner "Seed the development database with example data"
  #
  #   # other methods, etc.
  # end
  # ```
  macro name(name_text)
    def name
      {{name_text}}
    end
  end

  abstract def call
  abstract def banner
end

Add a newline to separate text from example code

abstract class LuckyCli::Task
  macro inherited
    LuckyCli::Runner.tasks << self.new

    def name
      "{{@type.name.gsub(/::/, ".").underscore}}"
    end
  end

  macro banner(banner_text)
    def banner
      {{banner_text}}
    end
  end

  # Sets a custom title for the task
  #
  # By default the name is derived from the full module and class name.
  # However if that name is not desired, a custom one can be set.
  #
  # ```
  # class Dev::Prime < LuckyCli::Task
  #   name "Development database primer"
  #   banner "Seed the development database with example data"
  #
  #   # other methods, etc.
  # end
  # ```
  macro name(name_text)
    def name
      {{name_text}}
    end
  end

  abstract def call
  abstract def banner
end


require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = websocket = HTTP::WebSocket.new(URI.parse(url))
      websocket.on_message(&->on_message(String))
      websocket.run
    end

    private def on_message(message : String)
    end
  end
end

Add a rudimentary close handler

require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = websocket = HTTP::WebSocket.new(URI.parse(url))
      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    private def on_message(message : String)
    end
  end
end


require "./server"

Authentic.configure do
  settings.secret_key = Lucky::Server.settings.secret_key_base

  unless Lucky::Env.production?
    settings.encryption_cost = 4
  end
end

Clarify what '4' represents in encryption cost

require "./server"

Authentic.configure do
  settings.secret_key = Lucky::Server.settings.secret_key_base

  unless Lucky::Env.production?
    fastest_encryption_possible = 4
    settings.encryption_cost = fastest_encryption_possible
  end
end


require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end
end

Write a test for MaybeSnowflakeConverter

require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

struct StructWithMaybeSnowflake
  JSON.mapping(
    data: {type: UInt64 | Nil, converter: Discord::MaybeSnowflakeConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end

  describe Discord::MaybeSnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end

    it "converts null to nil" do
      json = %({"data":null})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq nil
    end
  end
end


#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket
  puts "now here"
  get "/" do |ctx|
    send_file ctx, "src/web/index.html"
  end

  get "/app.js" do |ctx|
    send_file ctx, "src/web/app.js"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    puts "Received new connection!"
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us
      #SOCKETS.each { |socket| socket.send message}
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  puts "past redis"
  Kemal.run
end

Clean up code and comments

#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  # The Kemal app, pretty straightforward.

  # For root and /app.js we just hardcode some paths. You should probably use a nicer way for this.
  get "/" do |ctx|
    send_file ctx, "src/web/index.html"
  end

  get "/app.js" do |ctx|
    send_file ctx, "src/web/app.js"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us, but leaving this in as an
      # example for how you could do stuff when people send you messages
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  Kemal.run
end


require "ecr/macros"

module Artanis
  # TODO: render views in subpaths (eg: views/blog/posts/show.ecr => render_blog_posts_show_ecr)
  module Render
    macro ecr(name, layout = "layout")
      render {{ name }}, "ecr", layout: {{ layout }}
    end

    macro render(name, engine, layout = "layout")
      {% if layout %}
        render_{{ layout.id }}_{{ engine.id }} do
          render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
        end
      {% else %}
        render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
      {% end %}
    end

    macro views_path(path)
      {%
        views = `cd #{ path } 2>/dev/null && find . -name "*.ecr" -type f | cut -c 3- || true`
          .lines
          .map(&.strip.gsub(/\.ecr/, "").gsub(/\//, "__SLASH__"))
      %}

      {% for view in views %}
        def render_{{ view.id }}_ecr
          render_{{ view.id }}_ecr {}
        end

        def render_{{ view.id }}_ecr(&block)
          String.build do |__str__|
            embed_ecr "{{ path.id }}/{{ view.gsub(/__SLASH__/, "/").id }}.ecr", "__str__"
          end
        end
      {% end %}
    end
  end
end

Replace cut command with Crystal code

require "ecr/macros"

module Artanis
  # TODO: render views in subpaths (eg: views/blog/posts/show.ecr => render_blog_posts_show_ecr)
  module Render
    macro ecr(name, layout = "layout")
      render {{ name }}, "ecr", layout: {{ layout }}
    end

    macro render(name, engine, layout = "layout")
      {% if layout %}
        render_{{ layout.id }}_{{ engine.id }} do
          render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
        end
      {% else %}
        render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
      {% end %}
    end

    macro views_path(path)
      {%
        views = `cd #{ path } 2>/dev/null && find . -name "*.ecr" -type f || true`
          .lines
          .map(&.strip
            .gsub(/^\.\//, "")
            .gsub(/\.ecr/, "")
            .gsub(/\//, "__SLASH__"))
      %}

      {% for view in views %}
        def render_{{ view.id }}_ecr
          render_{{ view.id }}_ecr {}
        end

        def render_{{ view.id }}_ecr(&block)
          String.build do |__str__|
            embed_ecr "{{ path.id }}/{{ view.gsub(/__SLASH__/, "/").id }}.ecr", "__str__"
          end
        end
      {% end %}
    end
  end
end


require "http"

class Kemal::CommonLogHandler < Kemal::BaseLogHandler
  @handler : IO::FileDescriptor
  getter handler

  def initialize(@env)
    @handler = if @env == "production"
                 handler = File.new("kemal.log", "a")
                 handler.flush_on_newline = true
                 handler
               else
                 STDOUT
               end
  end

  def call(context)
    time = Time.now
    call_next(context)
    elapsed = Time.now - time
    elapsed_text = elapsed_text(elapsed)
    write "#{time} #{context.response.status_code} #{context.request.method} #{context.request.resource} - #{elapsed_text}\n"
    context
  end

  private def elapsed_text(elapsed)
    minutes = elapsed.total_minutes
    return "#{minutes.round(2)}m" if minutes >= 1

    seconds = elapsed.total_seconds
    return "#{seconds.round(2)}s" if seconds >= 1

    millis = elapsed.total_milliseconds
    return "#{millis.round(2)}ms" if millis >= 1

    "#{(millis * 1000).round(2)}µs"
  end

  def write(message)
    if @env == "production"
      @handler.write message.to_slice
    else
      @handler.print message
    end
  end
end

Optimize CommonLogHandler to directly use the handler instead of string interpolation

require "http"

class Kemal::CommonLogHandler < Kemal::BaseLogHandler
  @handler : IO::FileDescriptor
  getter handler

  def initialize(@env)
    @handler = if @env == "production"
                 handler = File.new("kemal.log", "a")
                 handler.flush_on_newline = true
                 handler
               else
                 STDOUT
               end
  end

  def call(context)
    time = Time.now
    call_next(context)
    elapsed_text = elapsed_text(Time.now - time)
    @handler << time << " " << context.response.status_code << " " << context.request.method << " " << context.request.resource << " " << elapsed_text << "\n"
    @handler.rewind
    context
  end

  private def elapsed_text(elapsed)
    minutes = elapsed.total_minutes
    return "#{minutes.round(2)}m" if minutes >= 1

    seconds = elapsed.total_seconds
    return "#{seconds.round(2)}s" if seconds >= 1

    millis = elapsed.total_milliseconds
    return "#{millis.round(2)}ms" if millis >= 1

    "#{(millis * 1000).round(2)}µs"
  end
end


require "commander"
require "logger"

require "./crow.cr"

cli = Commander::Command.new do |cmd|
  cmd.use = "crow"
  cmd.long = "Transpile Crystal to Flow (JS) code."

  cmd.flags.add do |flag|
    flag.name = "verbose"
    flag.short = "-v"
    flag.long = "--verbose"
    flag.default = false
    flag.description = "Enable more verbose logging."
  end

  cmd.flags.add do |flag|
    flag.name = "no-strict"
    flag.long = "--no-strict"
    flag.default = false
    flag.description = "Disable strict mode (allow fallback transpilation)."
  end

  cmd.run do |options, arguments|
    Crow.logger = Logger.new(STDERR)
    Crow.strict = options.bool["strict"] && !options.bool["no-strict"]

    basename = nil
    input = if arguments.size == 0
              STDIN.gets_to_end
            else
              input_path = arguments[0]
              basename = File.basename(input_path).gsub(/\.[^.]+$/, "")

              File.read(input_path)
            end
    output = Crow.convert(input)

    if basename
      output_path = "#{basename}.js.flow"
      File.write(output_path, output)
    else
      STDOUT.puts output
    end
  end
end

Commander.run(cli, ARGV)

Replace -v flag with version in CLI

require "commander"
require "logger"

require "./crow.cr"

cli = Commander::Command.new do |cmd|
  cmd.use = "crow"
  cmd.long = "Transpile Crystal to Flow (JS) code."

  cmd.flags.add do |flag|
    flag.name = "version"
    flag.short = "-v"
    flag.long = "--version"
    flag.default = false
    flag.description = "Print version number and exit"
  end

  cmd.flags.add do |flag|
    flag.name = "no-strict"
    flag.long = "--no-strict"
    flag.default = false
    flag.description = "Disable strict mode (allow fallback transpilation)."
  end

  cmd.run do |options, arguments|
    if options.bool["version"]
      STDOUT.puts "Crow v#{Crow::VERSION}"
      next
    end

    Crow.logger = Logger.new(STDERR)
    Crow.strict = options.bool["strict"] && !options.bool["no-strict"]

    basename = nil
    input = if arguments.size == 0
              STDIN.gets_to_end
            else
              input_path = arguments[0]
              basename = File.basename(input_path).gsub(/\.[^.]+$/, "")

              File.read(input_path)
            end
    output = Crow.convert(input)

    if basename
      output_path = "#{basename}.js.flow"
      File.write(output_path, output)
    else
      STDOUT.puts output
    end
  end
end

Commander.run(cli, ARGV)


require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

Add a method to PartialUser to check whether it has all data

require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )

    def full? : Bool
      !@username.nil? && !@discriminator.nil? && !@avatar.nil?
    end
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end


require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_DISPATCH = 0
    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      when OP_DISPATCH
        handle_dispatch(json["t"], json["d"])
      else
        puts "Unsupported message: #{message}"
      end

      nil
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end

    private def handle_dispatch(type, data)
    end
  end
end

Add basic READY handling to the dispatch handler

require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_DISPATCH = 0
    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      when OP_DISPATCH
        handle_dispatch(json["t"], json["d"])
      else
        puts "Unsupported message: #{message}"
      end

      nil
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end

    private def handle_dispatch(type, data)
      case type
      when "READY"
        puts "Received READY, v: #{data["v"]}"
      else
        puts "Unsupported dispatch: #{type} #{data}"
      end
    end
  end
end


module Kemal::Exceptions
  class RouteNotFound < Exception
    def initialize(context)
      super "Requested path: '#{context.request.override_method as String}:#{context.request.path}' was not found."
    end
  end

  class CustomException < Exception
    getter context

    def initialize(@context)
      super "Rendered error with #{@context.response.status_code}"
    end
  end
end

Add instance types for Crystal 0.16.0

module Kemal::Exceptions
  class RouteNotFound < Exception
    def initialize(context)
      super "Requested path: '#{context.request.override_method as String}:#{context.request.path}' was not found."
    end
  end

  class CustomException < Exception

    def initialize(context)
      super "Rendered error with #{context.response.status_code}"
    end
  end
end


require "json"

module Discordcr
  module Mappings
  end
end

Create a mapping for the gateway request response

require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    class GatewayResponse
      JSON.mapping (
        url: String
      )
    end
  end
end