require "./spec_helper"

describe Interpreter do
  it "runs the spec suite" do

    # Build the interpreter
    Process.run("crystal", [
      "build",
      "src/charly.cr",
      "--release",
      "--stats",
      "-o bin/charly"
    ], output: STDOUT, input: STDIN, error: STDERR)

    # Set the environment variable for the std-lib
    Process.run("export", [
      "CHARLYDIR=./src/charly/std-lib"
    ])

    # Run the spec
    result = Process.run("./bin/charly", [
      "test/main.charly"
    ], output: STDOUT, input: STDIN, error: STDERR)

    result.exit_status.should eq(0)
  end
end

require "./spec_helper"

describe Interpreter do
  it "runs the spec suite" do

    # Create the bin directory
    Process.run("mkdir", [
      "bin"
    ])

    # Build the interpreter
    Process.run("crystal", [
      "build",
      "src/charly.cr",
      "--release",
      "--stats",
      "-o bin/charly"
    ], output: STDOUT, input: STDIN, error: STDERR)

    # Set the environment variable for the std-lib
    Process.run("export", [
      "CHARLYDIR=./src/charly/std-lib"
    ])

    # Run the spec
    result = Process.run("./bin/charly", [
      "test/main.charly"
    ], output: STDOUT, input: STDIN, error: STDERR)

    result.exit_status.should eq(0)
  end
end

Create the bin directory in the travis build


class MainLayout
  include LuckyWeb::Page
  include LuckyWeb::Layout

  @page : BasePage

  render do
    html_doctype

    html do
      head do
        utf8_charset
        title @page.page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
      end

      body do
        @page.render_inner
      end
    end
  end
end

class MainLayout
  include LuckyWeb::Page
  include LuckyWeb::Layout

  @page : BasePage

  render do
    html_doctype

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

      body do
        @page.render_inner
      end
    end
  end
end

Add default lang to layout

Add default lang to layout

Closes #46


require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    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

  struct GatewayPacket
    JSON.mapping(
      op: UInt8,
      d: Nil,
      s: UInt32 | Nil,
      t: String | Nil
    )
end

Add a mapping for gateway packets

Add a mapping for gateway packets


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, 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 "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, body : String | Nil)
      headers["Authorization"] = @token
      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

Set the Authorization header to the token


require "./../spec_helper"

describe M3U8::Playlist do
  describe "when creating a playlist" do
    let(playlist) { M3U8::Playlist.new }

    it "inserts the base header" do
      output = \
          "#EXTM3U\n" \
          "\n"

      expect(playlist.to_s).to eq(output)
    end

    describe "media playlist" do
      it "accepts new segments" do
        playlist.add_segment("S01E01-1080-0001.ts", 9.003)

        output = \
          "#EXTM3U\n" \
          "\n" \
          "#EXTINF:9.003,\n" \
          "S01E01-1080-0001.ts\n"

        expect(playlist.to_s).to eq(output)
      end
    end
  end
end

require "./../spec_helper"

describe M3U8::Playlist do
  describe "when creating a playlist" do
    let(playlist) { M3U8::Playlist.new }

    it "inserts the header" do
      expect(playlist.to_s).to match(/#EXTM3U\n/)
    end

    describe "media playlist" do
      it "accepts new segments" do
        playlist.add_segment("S01E01-1080-0001.ts", 9.003)

        output = "#EXTINF:9.003,\n" \
                 "S01E01-1080-0001.ts\n"

        expect(playlist.to_s).to match(/#{output}/)
      end
    end
  end
end

test: Validate independent parts instead of the whole output


struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end

  def []=(offset, value)
    (self + offset).value = value
  end
end

struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end

  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
end

Add safety constraints to Pointer

Add safety constraints to Pointer


require "./tree.cr"

module Crustache
  abstract class FileSystem
    abstract def load(value : String) : Tree::Template?

    def load!(value : String) : Tree::Template
      if tmpl = self.load value
        return tmpl
      else
        raise "#{value} is not found"
      end
    end
  end

  class HashFileSystem < FileSystem
    def initialize
      @tmpls = {} of String => Tree::Template
    end

    def register(name, tmpl)
      @tmpls[name] = tmpl
    end

    def load(value)
      return @tmpls[value]?
    end
  end

  class ViewLoader < FileSystem
    def initialize(@basedir : String, @use_cache = false)
      @cache = {} of String => Tree::Template?
    end

    def load(value)
      if @cache.has_key?(value)
        return @cache[value]
      end

      filename = "#{@basedir}/#{value}"
      filename_ext = "#{filename}.mustache"
      if File.exists?(filename_ext)
        tmpl = Crustache.parseFile filename_ext
        @cache[value] = tmpl if @use_cache
        return tmpl
      end

      if File.exists?(filename)
        tmpl = Crustache.parse filename
        @cache[value] = tmpl if @use_cache
        return tmpl
      end

      @cache[value] = nil if @use_cache
      return nil
    end
  end
end

require "./tree.cr"

module Crustache
  abstract class FileSystem
    abstract def load(value : String) : Tree::Template?

    def load!(value : String) : Tree::Template
      if tmpl = self.load value
        return tmpl
      else
        raise "#{value} is not found"
      end
    end
  end

  class HashFileSystem < FileSystem
    def initialize
      @tmpls = {} of String => Tree::Template
    end

    def register(name, tmpl)
      @tmpls[name] = tmpl
    end

    def load(value)
      return @tmpls[value]?
    end
  end

  class ViewLoader < FileSystem
    EXTENSION = [".mustache", ".html", ""]

    def initialize(@basedir : String, @use_cache = false, @extension = EXTENSION)
      @cache = {} of String => Tree::Template?
    end

    def load(value)
      if @cache.has_key?(value)
        return @cache[value]
      end

      @extension.each do |ext|
        filename = "#{@basedir}/#{value}"
        filename_ext = "#{filename}#{ext}"
        if File.exists?(filename_ext)
          tmpl = Crustache.parseFile filename_ext
          @cache[value] = tmpl if @use_cache
          return tmpl
        end
      end

      @cache[value] = nil if @use_cache
      return nil
    end
  end
end

Add extensions for implicit loading

Add extensions for implicit loading


class MockRequest < HTTP::Request
  def initialize

  end
end

class MockRequest

  def initialize
  end

  def get(path : String, headers = Hash.new : Hash(String, String | Array(String)))
    http_headers = HTTP::Headers.new

    headers.each do |key, value|
      http_headers.add(key, value)
    end

    HTTP::Request.new("GET", path, http_headers)
  end

end

Add a basic MockRequest for mocking a HTTP::Request


AMBER_PORT = ENV["PORT"]? || 3008
AMBER_ENV  = ENV["AMBER_ENV"]? || "development"

require "amber"
require "./initializers/*"

Amber::Server.instance.config do |app|
  app_path = __FILE__
  app.name = "Crystal [ANN] web application."
  app.port = AMBER_PORT.to_i
  app.env = AMBER_ENV.to_s
  app.log = ::Logger.new(STDOUT)
  app.log.level = ::Logger::INFO
end

Amber::Server.instance.session = {
  :key     => "crystal.ann.session",
  :store   => :cookie,
  :expires => 120,
  :secret  => ENV["AMBER_SESSION_SECRET"]? || "",
}

AMBER_PORT = ENV["PORT"]? || 3008
AMBER_ENV  = ENV["AMBER_ENV"]? || "development"

require "amber"
require "./initializers/*"

Amber::Server.instance.config do |app|
  app_path = __FILE__
  app.name = "Crystal [ANN] web application."
  app.port = AMBER_PORT.to_i
  app.env = AMBER_ENV.to_s
  app.log = ::Logger.new(STDOUT)
  app.log.level = ::Logger::INFO
end

Amber::Server.instance.session = {
  :key     => "crystal.ann.session",
  :store   => :cookie,
  :expires => 0,
  :secret  => ENV["AMBER_SESSION_SECRET"]? || "",
}

Expire session when browser is closed

Expire session when browser is closed


require "./crustache/*"

module Crustache
  # :nodoc:
  OPEN_TAG = "{{".to_slice
  # :nodoc:
  CLOSE_TAG = "}}".to_slice

  def self.parse(string : String, filename = "<inline>", row = 1) : Tree::Template
    self.parse StringIO.new(string), filename, row
  end

  def self.parse(io : IO, filename = "<inline>", row = 1) : Tree::Template
    Parser.new(OPEN_TAG, CLOSE_TAG, io, filename, row).parse
  end

  def self.parseFile(filename : String) : Tree::Template
    self.parse(File.new(filename), filename, 1)
  end

  def self.render(tmpl : Tree::Template, model, fs = FileSystem.new) : String
    String.build do |io|
      self.render tmpl, model, fs, io
    end
  end

  def self.render(tmpl : Tree::Template, model, fs : FileSystem, io : IO)
    tmpl.visit Renderer.new OPEN_TAG, CLOSE_TAG, [model], fs, io
  end
end

require "./crustache/*"

module Crustache
  # :nodoc:
  OPEN_TAG = "{{".to_slice
  # :nodoc:
  CLOSE_TAG = "}}".to_slice

  def self.parse(string : String, filename = "<inline>", row = 1) : Tree::Template
    self.parse StringIO.new(string), filename, row
  end

  def self.parse(io : IO, filename = "<inline>", row = 1) : Tree::Template
    Parser.new(OPEN_TAG, CLOSE_TAG, io, filename, row).parse
  end

  def self.parseFile(filename : String) : Tree::Template
    self.parse(File.new(filename), filename, 1)
  end

  def self.render(tmpl : Tree::Template, model, fs = HashFileSystem.new) : String
    String.build do |io|
      self.render tmpl, model, fs, io
    end
  end

  def self.render(tmpl : Tree::Template, model, fs : FileSystem, io : IO)
    tmpl.visit Renderer.new OPEN_TAG, CLOSE_TAG, [model], fs, io
  end
end

Fix a bug that FileSystem can't create instance


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

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
  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_EMOJI_UPDATE dispatch payload


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})"

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

      spawn { HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT) }
    end

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

    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 "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})"

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

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

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

    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

Remove the fiber spawn in request

Remove the fiber spawn in request


struct Int
    def <<(count) unsafe_shl count end
    def >>(count) unsafe_shr count end
    def /(other : Int)
        if other == 0
            self
        end
        unsafe_div other
    end
    def times
        i = 0
        while i < self
            yield i
            i += 1
        end
    end
end

struct Int
    def <<(count) unsafe_shl count end
    def >>(count) unsafe_shr count end
    def ===(other : Int) self == other end
    def /(other : Int)
        if other == 0
            self
        end
        unsafe_div other
    end
    def times
        i = 0
        while i < self
            yield i
            i += 1
        end
    end
end

Add === override to Int

Add === override to Int


require "./../spec_helper"

describe M3U8::MasterPlaylist do
  context "when rendering a playlist" do
    let(streams) { [ M3U8::MasterPlaylist::Stream.new("low.m3u8"),
                     M3U8::MasterPlaylist::Stream.new("medium.m3u8"),
                     M3U8::MasterPlaylist::Stream.new("high.m3u8") ] }
    let(playlist) { M3U8::MasterPlaylist.new(streams) }

    it "inserts the header" do
      expect(playlist.to_s).to match(/#EXTM3U\n/)
    end

    it "inserts the version tag" do
      expect(playlist.to_s).to match(/#EXT-X-VERSION:3\n/)
    end

    context "with streams" do
      it "renders multiple streams" do
        streams.each do |stream|
          expect(playlist.to_s).to match(/#{streams.to_s}/)
        end
      end

      it "allows to add streams on the fly" do
        new_streams = M3U8::MasterPlaylist::Stream.new("another.m3u8")
        playlist.streams << new_streams

        expect(playlist.to_s).to match(/#{new_streams.to_s}/)
      end
    end
  end
end

require "./../spec_helper"

describe M3U8::MasterPlaylist do
  context "when rendering a playlist" do
    let(streams) { [ M3U8::MasterPlaylist::Stream.new("low.m3u8"),
                     M3U8::MasterPlaylist::Stream.new("medium.m3u8"),
                     M3U8::MasterPlaylist::Stream.new("high.m3u8") ] }
    let(playlist) { M3U8::MasterPlaylist.new(streams) }

    it "inserts the header" do
      expect(playlist.to_s).to match(/#EXTM3U\n/)
    end

    it "inserts the version tag" do
      expect(playlist.to_s).to match(/#EXT-X-VERSION:3\n/)
    end

    it "inserts an empty line between the header and rest" do
      expect(playlist.to_s.lines[2]).to eq("\n")
    end

    context "with streams" do
      it "renders multiple streams" do
        streams.each do |stream|
          expect(playlist.to_s).to match(/#{streams.to_s}/)
        end
      end

      it "allows to add streams on the fly" do
        new_streams = M3U8::MasterPlaylist::Stream.new("another.m3u8")
        playlist.streams << new_streams

        expect(playlist.to_s).to match(/#{new_streams.to_s}/)
      end
    end
  end
end

test: Add scenario with empty line between header and the rest


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

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
  end
end

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

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
  end
end

Add a mapping for the GUILD_DELETE payload


require "./mappings/*"

module Discord
  class Cache
    def initialize(@client : Client)
      @users = Hash(UInt64, User).new
    end

    def resolve_user(id : UInt64) : User
      @users.fetch(id) { @users[id] = @client.get_user(id) }
    end

    def cache(user : User)
      @users[user.id] = user
    end
  end
end

require "./mappings/*"

module Discord
  class Cache
    def initialize(@client : Client)
      @users = Hash(UInt64, User).new
      @channels = Hash(UInt64, Channel).new
    end

    def resolve_user(id : UInt64) : User
      @users.fetch(id) { @users[id] = @client.get_user(id) }
    end

    def resolve_channel(id : UInt64) : Channel
      @channels.fetch(id) { @channels[id] = @client.get_channel(id) }
    end

    def cache(user : User)
      @users[user.id] = user
    end

    def cache(channel : Channel)
      @channels[channel.id] = channel
    end
  end
end

Add support for channel caching

Add support for channel caching


class Kemal::CommonExceptionHandler < HTTP::Handler
  INSTANCE = new

  def call(context)
    begin
      call_next context
    rescue ex : Kemal::Exceptions::RouteNotFound
      context.response.content_type = "text/html"
      Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace.colorize(:red)}\n")
      return render_404(context)
    rescue ex
      context.response.content_type = "text/html"
      Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace.colorize(:red)}\n")
      verbosity = Kemal.config.env == "production" ? false : true
      return render_500(context, ex.inspect_with_backtrace, verbosity)
    end
  end
end

class Kemal::CommonExceptionHandler < HTTP::Handler
  INSTANCE = new

  def call(context)
    begin
      call_next context
    rescue ex : Kemal::Exceptions::RouteNotFound
      context.response.content_type = "text/html"
      Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace}\n")
      return render_404(context)
    rescue ex
      context.response.content_type = "text/html"
      Kemal.config.logger.write("Exception: #{ex.inspect_with_backtrace}\n")
      verbosity = Kemal.config.env == "production" ? false : true
      return render_500(context, ex.inspect_with_backtrace, verbosity)
    end
  end
end

Remove colorize from common exception 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 = HTTP::WebSocket.new(URI.parse(url))
      @websocket.not_nil!.on_message(&->on_message(String)) # TODO: better error handling
      @websocket.not_nil!.run
    end

    private def on_message(message : String)
    end
  end
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

Use a local variant of the websocket to avoid the not_nil! checks


require "../spec_helper"

describe Crow do
  it "should convert simple case statements" do
    crystal = <<-CR
    value = "foo"
    case value
    when "foo"
      p "yes"
    when "bar"
      p "no"
    else
      p "hiyaa"
    end
    CR

    flow = <<-JS
    const value = "foo";
    switch (value) {
    case "foo":
      console.log("yes");
      break;
    case "bar":
      console.log("no");
      break;
    default:
      console.log("hiyaa");
      break;
    }
    JS

    Crow.convert(crystal).should eq flow
  end

  it "should raise an exception when conditionless case statements are found" do
    crystal = <<-CR
    case
    when true
      p "yes"
    when false
      p "no"
    else
      p "hiyaa"
    end
    CR

    exception_raised = false

    begin
      Crow.convert(crystal)
    rescue e
      exception_raised = true
      e.message.should match /Case statements without conditions cannot be transpiled./
      e.message.should match %r{See https://github.com/geppetto-apps/crow/issues/8/}
    ensure
      Crow.strict = false
    end

    exception_raised.should eq true
  end
end

require "../spec_helper"

describe Crow do
  it "should convert simple case statements" do
    crystal = <<-CR
    value = "foo"
    case value
    when "foo"
      p "yes"
    when "bar"
      p "no"
    else
      p "hiyaa"
    end
    CR

    flow = <<-JS
    const value = "foo";
    switch (value) {
    case "foo":
      console.log("yes");
      break;
    case "bar":
      console.log("no");
      break;
    default:
      console.log("hiyaa");
      break;
    }
    JS

    Crow.convert(crystal).should eq flow
  end

  it "should support case statements without default" do
    crystal = <<-CR
    value = "foo"
    case value
    when "foo"
      p "yes"
    when "bar"
      p "no"
    end
    CR

    flow = <<-JS
    const value = "foo";
    switch (value) {
    case "foo":
      console.log("yes");
      break;
    case "bar":
      console.log("no");
      break;
    }
    JS

    Crow.convert(crystal).should eq flow
  end

  it "should raise an exception when conditionless case statements are found" do
    crystal = <<-CR
    case
    when true
      p "yes"
    when false
      p "no"
    else
      p "hiyaa"
    end
    CR

    exception_raised = false

    begin
      Crow.convert(crystal)
    rescue e
      exception_raised = true
      e.message.should match /Case statements without conditions cannot be transpiled./
      e.message.should match %r{See https://github.com/geppetto-apps/crow/issues/8/}
    ensure
      Crow.strict = false
    end

    exception_raised.should eq true
  end
end

Add spec for missing else in case statement


module Indexable(T)
  abstract def size
  abstract def unsafe_at(index : Int)

  def at(index : Int)
    index = check_index_out_of_bounds index do
      return yield
    end
    unsafe_at index
  end

  @[AlwaysInline]
  def at(index : Int)
    at(index) { panic "Invalid index!" }
  end

  @[AlwaysInline]
  def [](index : Int)
    at index
  end

  @[AlwaysInline]
  def []?(index : Int)
    at index { nil }
  end

  @[AlwaysInline]
  def []=(index : Int, value : T)
    index = check_index_out_of_bounds index
    @buffer[index] = value
  end

  def each
    each_index do |i|
      yield unsafe_at i
    end
  end

  def each_index
    i = 0
    while i < size
      yield i
      i += 1
    end
    self
  end

  def empty?
    size == 0
  end

  private def check_index_out_of_bounds(index)
    check_index_out_of_bounds(index) { panic "Index out of bounds!" }
  end

  private def check_index_out_of_bounds(index)
    index += size if index < 0
    if 0 <= index < size
      index
    else
      yield
    end
  end
end

module Indexable(T)
  abstract def size
  abstract def unsafe_at(index : Int)

  def at(index : Int)
    index = check_index_out_of_bounds index do
      return yield
    end
    unsafe_at index
  end

  @[AlwaysInline]
  def at(index : Int)
    at(index) { panic "Invalid index!" }
  end

  @[AlwaysInline]
  def [](index : Int)
    at index
  end

  @[AlwaysInline]
  def []?(index : Int)
    at(index) { nil }
  end

  @[AlwaysInline]
  def []=(index : Int, value : T)
    index = check_index_out_of_bounds index
    @buffer[index] = value
  end

  def each
    each_index do |i|
      yield unsafe_at i
    end
  end

  def each_index
    i = 0
    while i < size
      yield i
      i += 1
    end
    self
  end

  def empty?
    size == 0
  end

  private def check_index_out_of_bounds(index)
    check_index_out_of_bounds(index) { panic "Index out of bounds!" }
  end

  private def check_index_out_of_bounds(index)
    index += size if index < 0
    if 0 <= index < size
      index
    else
      yield
    end
  end
end

Fix a bug in Indexable(T)
This fixes a bug where calling `Indexable(T).[]?` wouldn't compile.

Signed-off-by: Splitty <e8820feff88402cc56882fceaf96271e952e1a96@gmail.com>


require "./hooks_dsl"
require "./config_dsl"

module Soil
  class App
    extend HooksDSL
    extend RoutingDSL
    extend ConfigDSL

    @@namespace = ""
    @@before_hooks = [] of Http::Request, Http::Response ->
    @@after_hooks = [] of Http::Request, Http::Response ->
    @@router = Router.new
    @@config = Config.new
    @@logger = Logger.new(STDOUT)

    def initialize
      @server = HTTP::Server.new(
        @@config.host,
        @@config.port,
        [
          HTTP::LogHandler.new,
          HTTP::ErrorHandler.new,
          Http::MainHandler.new(self.class)
        ]
      )
    end

    def self.routes
      @@router.routes
    end

    def self.wrap_within_self_callables(callables)
      before_hooks + callables + after_hooks
    end

    def self.mount_route(method : String, path : String, callables)
      path = [@@namespace, path].join("/")
      all_callables = wrap_within_self_callables(callables)
      @@router.add(method, path, all_callables)
    end

    def self.find(request)
      @@router.find(request)
    end

    def self.namespace(name)
      @@namespace = name
    end

    def self.configuration
      @@config
    end

    def self.logger
      @@logger
    end

    def run
      self.class.logger.info(
        "An app emerged from soil running on #{self.class.configuration.host}:#{@server.port}"
      )
      @server.listen
    end
  end
end

require "./hooks_dsl"
require "./config_dsl"

module Soil
  class App
    extend HooksDSL
    extend RoutingDSL
    extend ConfigDSL

    @@namespace = ""
    @@before_hooks = [] of Http::Request, Http::Response ->
    @@after_hooks = [] of Http::Request, Http::Response ->
    @@router = Router.new
    @@config = Config.new
    @@logger = Logger.new(STDOUT)

    def initialize
      @server = HTTP::Server.new(
        @@config.host,
        @@config.port,
        [
          HTTP::LogHandler.new,
          HTTP::ErrorHandler.new,
          Http::MainHandler.new(self.class)
        ]
      )
    end

    def self.routes
      @@router.routes
    end

    def self.wrap_within_self_callables(callables)
      before_hooks + callables + after_hooks
    end

    def self.mount_route(method : String, path : String, callables)
      path = [@@namespace, path].join("/")
      all_callables = wrap_within_self_callables(callables)
      @@router.add(method, path, all_callables)
    end

    def self.find(request)
      @@router.find(request)
    end

    def self.namespace(name)
      @@namespace = name
    end

    def self.configuration
      @@config
    end

    def self.logger
      @@logger
    end

    def self.run
      app = self.new
      app.run
    end

    def run
      self.class.logger.info(
        "An app emerged from soil running on #{self.class.configuration.host}:#{@server.port}"
      )
      @server.listen
    end
  end
end

Add class method to simplify boot

Add class method to simplify boot


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

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

  abstract def inner

  render do
    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

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

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User
  needs flash : LuckyWeb::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

Use render method instead of block in app skeleton


abstract class BaseAction < LuckyWeb::Action
  include LuckyWeb::Exposeable

  macro inherited
    # If something should always be exposed to your pages, expose them here.
    #
    # Example:
    #
    #  macro inherited
    #    expose :current_user
    #  end
    #
    #  def current_user
    #    find_the_user...
    #  end
    #
    # Then add an assign for it in your BasePage
  end
end

abstract class BaseAction < LuckyWeb::Action
  # If something should always be exposed to your pages, expose them here.
  #
  # Example:
  #
  #  expose current_user
  #
  #  def current_user
  #    find_the_user...
  #  end
  #
  # Then add an assign for it in your BasePage
end

Make exposures work with latest LuckyWeb


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 "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

Fix a syntax error in the request parameters


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

  # Converts a value that may be a snowflake, but might also be nil, to a
  # UInt64.
  module MaybeSnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64?
      str = parser.read_string_or_null

      if str
        str.to_u64
      else
        nil
      end
    end

    def self.to_json(value : UInt64?, io : IO)
      if value
        io.puts(value.to_s)
      else
        io.puts("null")
      end
    end
  end

  # Converts an array of strings to an array of UInt64s.
  module SnowflakeArrayConverter
    def self.from_json(parser : JSON::PullParser) : Array(UInt64)
      Array(String).new(parser).map &.to_u64
    end

    def self.to_json(value : Array(UInt64), io : IO)
      value.map(&.to_s).to_json(io)
    end
  end
end

require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      parser.read_string.to_u64
    end

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

  # Converts a value that may be a snowflake, but might also be nil, to a
  # UInt64.
  module MaybeSnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64?
      str = parser.read_string_or_null

      if str
        str.to_u64
      else
        nil
      end
    end

    def self.to_json(value : UInt64?, io : IO)
      if value
        io.puts(value.to_s)
      else
        io.puts("null")
      end
    end
  end

  # Converts an array of strings to an array of UInt64s.
  module SnowflakeArrayConverter
    def self.from_json(parser : JSON::PullParser) : Array(UInt64)
      Array(String).new(parser).map &.to_u64
    end

    def self.to_json(value : Array(UInt64), io : IO)
      value.map(&.to_s).to_json(io)
    end
  end
end

Remove an unnecessary not_nil! from SnowflakeConverter


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

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

Fix bug with wrong variable name in Hero.cr


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/**"

Add missing require in Amber integration


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

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

Fix for the 031aff... commit

Fix for the 031aff... commit


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

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

Add type requirements to all the endpoint methods


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

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

Fix compatibility with Crystal 0.34 stdlib


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

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

Add session and flash handlers

Add session and flash handlers

Closes #61


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

include Kemal

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

Set to development before_each spec

Set to development before_each spec


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

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

Improve messaging when SEND_GRID_KEY is missing


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

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

Disable query logging for `crystal spec`


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 "./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

Write a test for SnowflakeArrayConverter


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

Clean up code and add comments


module Discordcr
  module REST
  end
end

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

Implement a very basic request method

Implement a very basic request method


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

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

Handle PUT and DELETE methods

Handle PUT and DELETE methods

Closes #29


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/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

Add a REST method to send a message

Add a REST method to send a message


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

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

Update the Crystal example for Crystal 0.31.0


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

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

Remove redundant transpile(content : String)


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

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

Set Content-Type after call_next in Kemal::InitHandler


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

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

Send username provided by kemalcr/kemal-basic-auth


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

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

Remove accidentally included webpack dep


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

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

Add <, > and to_byte_ptr functions to Pointer


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

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 email field to the User mapping (for OAuth2)


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

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

Add to_s method for Params

Add to_s method for Params


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

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

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

Otherwise it will crash with a failed nil assertion.


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

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

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


#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 "./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

Change endpoints in the app to use the /public directory


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

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

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


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

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

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


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

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

Return HTTP error 400 if the filename is broken


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

include Spec2::GlobalDSL

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

Use spec2 doc reporter for specs

chore: Use spec2 doc reporter for specs


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

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

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


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

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

Fix wrong filter name in filter documentation


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

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 TODO to remind me of the body_io problem


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

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

Make sure to convert the heartbeat_interval to an int


# 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

# 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

Use localhost for the RouteHelper

Use localhost for the RouteHelper

This is what you want most of the time, especially when using for callback Urls with OAuth apps


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

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

Append newline after nodes with children

Signed-off-by: Leonard Schuetz <0a869347170afd1ee7c6a6a90d899177d625e0cf@me.com>


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

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

Fix return type restriction since Crystal 0.35.0

Fix #27


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

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

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


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

    def run
    end
  end
end

require "./rest"

module Discordcr
  class Client
    include REST

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

    def run
    end
  end
end

Include the REST module in Client

Include the REST module in Client


require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping (
        url: String
      )
    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


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

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

Fix RemoveCircularReferences to play nice with types


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

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

Fix wrong argument usage in HTTPExceptions


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

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

Disable turbolinks cache by default

Disable turbolinks cache by default

Closes #357


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

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

Add version information on errors

Add version information on errors