Genie.jl icon indicating copy to clipboard operation
Genie.jl copied to clipboard

Move some initializers into Genie (?)

Open andyDoucette opened this issue 3 years ago • 4 comments

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?

andyDoucette avatar Sep 26 '21 22:09 andyDoucette

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).

essenciary avatar Sep 27 '21 09:09 essenciary

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.

andyDoucette avatar Sep 28 '21 01:09 andyDoucette

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.

essenciary avatar Sep 29 '21 13:09 essenciary

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.

andyDoucette avatar Sep 30 '21 01:09 andyDoucette