Skip to content

Commit

Permalink
Add crystal lucky (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
hahwul committed Nov 20, 2023
1 parent e756da7 commit 6390b67
Show file tree
Hide file tree
Showing 21 changed files with 429 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
| Language | Framework | URL | Method | Param | Header | WS |
|----------|-----------------|-----|--------|-------|--------|----|
| Crystal | Kemal ||||||
| Crystal | Lucky ||||| |
| Go | Echo ||||| X |
| Go | Gin ||||| X |
| Python | Django ||||| X |
Expand Down
11 changes: 11 additions & 0 deletions spec/functional_test/fixtures/crystal_lucky/config/authentic.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "./server"

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

unless LuckyEnv.production?
# This value can be between 4 and 31
fastest_encryption_possible = 4
settings.encryption_cost = fastest_encryption_possible
end
end
25 changes: 25 additions & 0 deletions spec/functional_test/fixtures/crystal_lucky/config/cookies.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require "./server"

Lucky::Session.configure do |settings|
settings.key = "_crystal_lucky_session"
end

Lucky::CookieJar.configure do |settings|
settings.on_set = ->(cookie : HTTP::Cookie) {
# If ForceSSLHandler is enabled, only send cookies over HTTPS
cookie.secure(Lucky::ForceSSLHandler.settings.enabled)

# By default, don't allow reading cookies with JavaScript
cookie.http_only(true)

# Restrict cookies to a first-party or same-site context
cookie.samesite(:lax)

# Set all cookies to the root path by default
cookie.path("/")

# You can set other defaults for cookies here. For example:
#
# cookie.expires(1.year.from_now).domain("mydomain.com")
}
end
Empty file.
33 changes: 33 additions & 0 deletions spec/functional_test/fixtures/crystal_lucky/shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: crystal_lucky
version: 0.1.0
targets:
crystal_lucky:
main: src/crystal_lucky.cr
crystal: '>= 1.10.1'
dependencies:
lucky:
github: luckyframework/lucky
version: ~> 1.1.0
avram:
github: luckyframework/avram
version: ~> 1.1.0
carbon:
github: luckyframework/carbon
version: ~> 0.4.0
carbon_sendgrid_adapter:
github: luckyframework/carbon_sendgrid_adapter
version: ~> 0.4.0
lucky_env:
github: luckyframework/lucky_env
version: ~> 0.2.0
lucky_task:
github: luckyframework/lucky_task
version: ~> 0.3.0
authentic:
github: luckyframework/authentic
version: '>= 1.0.0, < 2.0.0'
jwt:
github: crystal-community/jwt
version: ~> 1.6.0
development_dependencies: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Api::Me::Show < ApiAction
get "/api/me" do
remote_ip = request.headers["X-Forwarded-For"]
_ = remote_ip
params.from_query["q"] # => "Lucky"
params.get("query")
params.get(:filter)
json UserSerializer.new(current_user)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Api::SignIns::Create < ApiAction
include Api::Auth::SkipRequireAuthToken

post "/api/sign_ins" do
SignInUser.run(params) do |operation, user|
params.from_json["users"]
if user
json({token: UserToken.generate(user)})
else
raise Avram::InvalidOperationError.new(operation)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Api::SignUps::Create < ApiAction
include Api::Auth::SkipRequireAuthToken

post "/api/sign_ups" do
user = SignUpUser.create!(params)
json({token: UserToken.generate(user)})
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Include modules and add methods that are for all API requests
abstract class ApiAction < Lucky::Action
# APIs typically do not need to send cookie/session data.
# Remove this line if you want to send cookies in the response header.
disable_cookies
accepted_formats [:json]

include Api::Auth::Helpers

# By default all actions require sign in.
# Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests.
include Api::Auth::RequireAuthToken

# By default all actions are required to use underscores to separate words.
# Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
include Lucky::EnforceUnderscoredRoute
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This class handles error responses and reporting.
#
# https://luckyframework.org/guides/http-and-routing/error-handling
class Errors::Show < Lucky::ErrorAction
DEFAULT_MESSAGE = "Something went wrong."
default_format :json
dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]

def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
error_json "Not found", status: 404
end

# When an InvalidOperationError is raised, show a helpful error with the
# param that is invalid, and what was wrong with it.
def render(error : Avram::InvalidOperationError)
error_json \
message: error.renderable_message,
details: error.renderable_details,
param: error.invalid_attribute_name,
status: 400
end

# Always keep this below other 'render' methods or it may override your
# custom 'render' methods.
def render(error : Lucky::RenderableError)
error_json error.renderable_message, status: error.renderable_status
end

# If none of the 'render' methods return a response for the raised Exception,
# Lucky will use this method.
def default_render(error : Exception) : Lucky::Response
error_json DEFAULT_MESSAGE, status: 500
end

private def error_json(message : String, status : Int, details = nil, param = nil)
json ErrorSerializer.new(message: message, details: details, param: param), status: status
end

private def report(error : Exception) : Nil
# Send to Rollbar, send an email, etc.
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Home::Index < ApiAction
include Api::Auth::SkipRequireAuthToken

get "/" do
json({hello: "Hello World from Home::Index"})
end
end
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Api::Auth::Helpers
# The 'memoize' macro makes sure only one query is issued to find the user
memoize def current_user? : User?
auth_token.try do |value|
user_from_auth_token(value)
end
end

private def auth_token : String?
bearer_token || token_param
end

private def bearer_token : String?
context.request.headers["Authorization"]?
.try(&.gsub("Bearer", ""))
.try(&.strip)
end

private def token_param : String?
params.get?(:auth_token)
end

private def user_from_auth_token(token : String) : User?
UserToken.decode_user_id(token).try do |user_id|
UserQuery.new.id(user_id).first?
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Api::Auth::RequireAuthToken
macro included
before require_auth_token
end

private def require_auth_token
if current_user?
continue
else
json auth_error_json, 401
end
end

private def auth_error_json
ErrorSerializer.new(
message: "Not authenticated.",
details: auth_error_details
)
end

private def auth_error_details : String
if auth_token
"The provided authentication token was incorrect."
else
"An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header."
end
end

# Tells the compiler that the current_user is not nil since we have checked
# that the user is signed in
private def current_user : User
current_user?.as(User)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Api::Auth::SkipRequireAuthToken
macro included
skip require_auth_token
end

# Since sign in is not required, current_user might be nil
def current_user : User?
current_user?
end
end
25 changes: 25 additions & 0 deletions spec/functional_test/fixtures/crystal_lucky/tasks.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This file loads your app and all your tasks when running 'lucky'
#
# Run 'lucky --help' to see all available tasks.
#
# Learn to create your own tasks:
# https://luckyframework.org/guides/command-line-tasks/custom-tasks

# See `LuckyEnv#task?`
ENV["LUCKY_TASK"] = "true"

# Load Lucky and the app (actions, models, etc.)
require "./src/app"
require "lucky_task"

# You can add your own tasks here in the ./tasks folder
require "./tasks/**"

# Load migrations
require "./db/migrations/**"

# Load Lucky tasks (dev, routes, etc.)
require "lucky/tasks/**"
require "avram/lucky/tasks"

LuckyTask::Runner.run
19 changes: 19 additions & 0 deletions spec/functional_test/testers/crystal_lucky_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "../func_spec.cr"

extected_endpoints = [
Endpoint.new("/", "GET"),
Endpoint.new("/secret.html", "GET"),
Endpoint.new("/api/me", "GET", [
Param.new("q", "", "query"),
Param.new("query", "", "query"),
Param.new("filter", "", "query"),
Param.new("X-Forwarded-For", "", "header"),
]),
Endpoint.new("/api/sign_ins", "POST", [Param.new("users", "", "json")]),
Endpoint.new("/api/sign_ups", "POST"),
]

FunctionalTester.new("fixtures/crystal_lucky/", {
:techs => 1,
:endpoints => 5,
}, extected_endpoints).test_all
1 change: 1 addition & 0 deletions src/analyzer/analyzer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def initialize_analyzers(logger : NoirLogger)
analyzers["python_django"] = ->analyzer_django(Hash(Symbol, String))
analyzers["js_express"] = ->analyzer_express(Hash(Symbol, String))
analyzers["crystal_kemal"] = ->analyzer_kemal(Hash(Symbol, String))
analyzers["crystal_lucky"] = ->analyzer_crystal_lucky(Hash(Symbol, String))
analyzers["oas2"] = ->analyzer_oas2(Hash(Symbol, String))
analyzers["oas3"] = ->analyzer_oas3(Hash(Symbol, String))
analyzers["raml"] = ->analyzer_raml(Hash(Symbol, String))
Expand Down
Loading

0 comments on commit 6390b67

Please sign in to comment.