Genie.jl
Genie.jl copied to clipboard
Move some initializers into Genie (?)
Hello. I'm a newcomer to Genie. I found it a bit off-putting when I created a new full-stack app and it got populated with a lot of code that I know nothing about. Usually when I write an app, I want a hard separation between "other people's code" and "my code". I want the code that I see and that I work with on a day to day basis to be stuff that I wrote and fully understand. Populating a "fresh" repo with a bunch of other people's code is a good way to make people feel overwhelmed. Usually, with other libraries, this is solved by abstracting the functoinality in a module and just interfacing with it. Is it possible to more of this stuff to the library and have less of it copy-pasted to everyone's individual repos?
Here's an example of what I'm talking about. I'm just listing the .jl files but there's a lot more if I consider other stuff.
$ for file in $(find . -name '*.jl' ); do echo -e "\n===============\n$file\n=="; cat "$file"; done
===============
./app/helpers/ValidationHelper.jl
==
module ValidationHelper
using Genie
using SearchLight.Validation
export output_errors
function output_errors(m, field::Symbol) :: String
Validation.haserrorsfor(m, field) ? """<label class="control-label error label-danger">$(Validation.errors_to_string(m, field, "<br/>\n", uppercase_first = true))</label>""" : ""
end
end
===============
./app/helpers/ViewHelper.jl
==
module ViewHelper
using Genie, Genie.Flash, Genie.Router
export output_flash
function output_flash(flashtype::String = "danger") :: String
flash_has_message() ? """<div class="alert alert-$flashtype alert-dismissable">$(flash())</div>""" : ""
end
end
===============
./config/env/dev.jl
==
using Genie.Configuration, Logging
const config = Settings(
server_port = 8000,
server_host = "127.0.0.1",
log_level = Logging.Info,
log_to_file = false,
server_handle_static_files = true
)
ENV["JULIA_REVISE"] = "auto"
===============
./config/env/global.jl
==
# Place here configuration options that will be set for all environments
===============
./config/env/prod.jl
==
using Genie.Configuration, Logging
const config = Settings(
server_port = 8000,
server_host = "0.0.0.0",
log_level = Logging.Error,
log_to_file = true,
server_handle_static_files = true
)
if config.server_handle_static_files
@warn("For performance reasons Genie should not serve static files (.css, .js, .jpg, .png, etc) in production.
It is recommended to set up Apache or Nginx as a reverse proxy and cache to serve static assets.")
end
ENV["JULIA_REVISE"] = "off"
===============
./config/env/test.jl
==
using Genie.Configuration, Logging
const config = Settings(
server_port = 8000,
server_host = "127.0.0.1",
log_level = Logging.Debug,
log_to_file = true,
server_handle_static_files = true
)
ENV["JULIA_REVISE"] = "off"
===============
./config/initializers/autoload.jl
==
# Optional flat/non-resource MVC folder structure
# push!(LOAD_PATH, "controllers", "views", "views/layouts", "models")
===============
./config/initializers/converters.jl
==
using Dates
import Base.convert
convert(::Type{Int}, v::SubString{String}) = parse(Int, v)
convert(::Type{Float64}, v::SubString{String}) = parse(Float64, v)
convert(::Type{Date}, s::String) = parse(Date, s)
===============
./config/initializers/inflector.jl
==
import Inflector, Genie
if ! isempty(Genie.config.inflector_irregulars)
push!(Inflector.IRREGULAR_NOUNS, Genie.config.inflector_irregulars...)
end
===============
./config/initializers/logging.jl
==
import Genie
import Logging, LoggingExtras
import Dates
function initialize_logging()
date_format = "yyyy-mm-dd HH:MM:SS"
logger = if Genie.config.log_to_file
isdir(Genie.config.path_log) || mkpath(Genie.config.path_log)
LoggingExtras.TeeLogger(
LoggingExtras.FileLogger(joinpath(Genie.config.path_log, "$(Genie.config.app_env)-$(Dates.today()).log"), always_flush = true, append = true),
Logging.ConsoleLogger(stdout, Genie.config.log_level)
)
else
Logging.ConsoleLogger(stdout, Genie.config.log_level)
end
timestamp_logger(logger) = LoggingExtras.TransformerLogger(logger) do log
merge(log, (; message = "$(Dates.format(Dates.now(), date_format)) $(log.message)"))
end
LoggingExtras.TeeLogger(LoggingExtras.MinLevelLogger(logger, Genie.config.log_level)) |> timestamp_logger |> global_logger
nothing
end
initialize_logging()
===============
./config/initializers/searchlight.jl
==
using SearchLight
try
SearchLight.Configuration.load()
if SearchLight.config.db_config_settings["adapter"] !== nothing
eval(Meta.parse("using SearchLight$(SearchLight.config.db_config_settings["adapter"])"))
SearchLight.connect()
@eval begin
using Genie.Renderer.Json
function Genie.Renderer.Json.JSON3.StructTypes.StructType(::Type{T}) where {T<:SearchLight.AbstractModel}
Genie.Renderer.Json.JSON3.StructTypes.Struct()
end
function Genie.Renderer.Json.JSON3.StructTypes.StructType(::Type{SearchLight.DbId})
Genie.Renderer.Json.JSON3.StructTypes.Struct()
end
end
end
catch ex
@error ex
end
===============
./config/initializers/ssl.jl
==
using Genie, MbedTLS
function configure_dev_ssl()
cert = Genie.Assets.embedded_path(joinpath("files", "ssl", "localhost.crt")) |> MbedTLS.crt_parse_file
key = Genie.Assets.embedded_path(joinpath("files", "ssl", "localhost.key")) |> MbedTLS.parse_keyfile
ssl_config = MbedTLS.SSLConfig(true)
entropy = MbedTLS.Entropy()
rng = MbedTLS.CtrDrbg()
MbedTLS.config_defaults!(ssl_config, endpoint=MbedTLS.MBEDTLS_SSL_IS_SERVER)
MbedTLS.authmode!(ssl_config, MbedTLS.MBEDTLS_SSL_VERIFY_NONE)
MbedTLS.seed!(rng, entropy)
MbedTLS.rng!(ssl_config, rng)
MbedTLS.own_cert!(ssl_config, cert, key)
MbedTLS.ca_chain!(ssl_config)
Genie.config.ssl_config = ssl_config
nothing
end
Genie.Configuration.isdev() && Genie.config.ssl_enabled && configure_dev_ssl()
===============
./config/secrets.jl
==
Genie.secret_token!("79ec7a8cf0fe2efb54cc17b6d46e06c5a7e612558c49e545ee9e9bfde2091710")
===============
./routes.jl
==
using Genie.Router
route("/") do
serve_static_file("welcome.html")
end
===============
./src/MyGenieApp.jl
==
module MyGenieApp
using Genie, Logging, LoggingExtras
function main()
Core.eval(Main, :(const UserApp = $(@__MODULE__)))
Genie.genie(; context = @__MODULE__)
Core.eval(Main, :(const Genie = UserApp.Genie))
Core.eval(Main, :(using Genie))
end
end
===============
./test/runtests.jl
==
using MyGenieApp, Test
# implement your tests here
@test 1 == 1
===============
./bootstrap.jl
==
pwd() == joinpath(@__DIR__, "bin") && cd(@__DIR__) # allow starting app from bin/ dir
using MyGenieApp
MyGenieApp.main()
Ideally, any sort of "Make a fresh project for me" should create no more than 20-50 lines of code that are in the developer's repo, and the rest of it should be put in the library itself with proper ways to configure it using an interface besides copy-and-paste code.
Looking more closely, it seems it's the initializers and the helpers that would most benefit from being put back in Genie land instead of user-land. What would it take to do that?
Genie supports 4 main types of apps via Genie: newapp, newapp_fullstack, newapp_mvc, and newapp_webservice.
They provide various levels of "boilerplate". The guides explain the differences between them in detail: https://genieframework.github.io/Genie.jl/dev/
Maybe some of the initializers could be moved into Genie, not sure, must be tested (the ones that don't require user customization - logging, ssl, and searchlight). The helpers no as they are designed to be user editable (eg put specific code in there).
Thanks for your reply. The link you sent seems to go to the docs intro page. Is there a specific guide you wanted me to check out?
I see what you mean about the helpers - those are fairly short and I can see the usefulness of being able to override them. In flask, they have the default function in the package and then allow you to override it if you want to make your own. That really felt good to me when I was using flask, as I could start with an absolutely minimal thing and then only add to it once I fully understood what I was adding. It kinda kicks me in the gut to add code to my repo that I have no idea what it does or why it's there. If something breaks because of some interaction between my code and this code, I won't know why it broke and I'll spend hours debugging something. That's why there are interfaces, so there's a whole bunch of stuff you don't have to think about and just a little bit that you do and it's clear what's what. Having boilerplate code breaks this very helpful paradigm.
I guess this all comes down to what percentage of users can get away with the default for a function and which percentage will need to override it. I would say if 50% of the customers can make it 40h into building an app without customizing something, it should be in the library by default and not in the user's code space. It's very important to keep the cognitive load of using a library as low as possible in the first week or two ofuse, and as long as the documentation says exactly how to hook up their own code to override default behavior, it will take just a couple minutes for people to add their own overrides.
The helper functions are small enough that they're not so bad to digest.
How often are the initializes customized? Where should they go if they're not here? Are you willing to try to move them and test things out? At this point in my on-boarding process I feel like I would break more things than I would fix.
Helpers are not to be overwritten, rather extended. The current ones are more like examples, a decently sized app will need a lot more. The idea is that any complex view logic should go into helpers, as helper functions. https://mixandgo.com/learn/the-beginners-guide-to-rails-helpers
As for the initializers, some evolved over time and seems like there is no need for them to be user edited, so they could be moved into the libraries. However, it needs to be tested as scoping can be tricky. Breaking changes are acceptable, we can bump the version to v4 and introduce other breaking changes with this occasion.
Ok great! Would you be willing to do that? I'm still getting my feet wet and I'll likely break more things than I fix at this stage.
On Wed, Sep 29, 2021 at 9:24 PM Adrian Salceanu @.***> wrote:
Helpers are not to be overwritten, rather extended. The current ones are rather examples. The idea is that any complex view logic should go into helpers, as helper functions. https://mixandgo.com/learn/the-beginners-guide-to-rails-helpers
As for the initializers, some evolved over time and seems like there is no need for them to be user edited, so they could be moved into the libraries. However, it needs to be tested as scoping can be tricky. Breaking changes are acceptable, we can bump the version to v4 and introduce other breaking changes with this occasion.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/GenieFramework/Genie.jl/issues/415#issuecomment-930172266, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEVLQ5QFJS74S7LMHLAOHMTUEMHPZANCNFSM5EZISSXA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.