Check whether a required file / shard exists on Compilation time?

I was wondering if there’s possibility to sort of repeat this Ruby behavior but in crystal (and at compile time):

begin
  require "some_gem"
  require "./ext_for_some_gem" #< Require my code only if some_gem is available
rescue LoadError => e
  #Do not add my flavored code as the optional gem is not available
end

Which may look in crystal:

{% if require_exists?("some_shard") %}
   require "some_shard"
   require "./ext_for_some_shard"
{% end %}

This will allow also to create more beautiful compiler error code like this:

require "a_shard/ext/kemal"
# ^~~~ ERROR: You required ext/kemal but kemal wasn't found in your 
#             shards.yml ! Please install kemal first by adding:
#             -----
#               kemal:
#                 github: kemalcr/kemal
#             -----
#             in your shards.yml 

I don’t know if anything like this exists already in the macro system. Any thought about it?

2 Likes

It was discussed on Gitter. In 27.0.1 there will be read_file macro method which would raise on file missing – #6967 and #7094. As shards usually being stored in my_project/lib, you could check a shard’s existence like read_file?("lib/my_shard"). However, IIRC @ysbaddaden once mentioned that this would not work if shards aren’t in the lib folder. Thus I’d be happy if we had explicit require? "shard" method.

What’s the usecase? There’s no concept of optional import in most languages, so it doesn’t seem so necessary to me.

1 Like

What’s the usecase? There’s no concept of optional import in most languages, so it doesn’t seem so necessary to me.

Well I’m not agreeing about it. This concept is quite used in Ruby, but not only in Ruby. In some languages, there’s concept of optional import. Just yesterday, toying with dwm:

     #ifdef XINERAMA
     #include <X11/extensions/Xinerama.h>
     #endif /* XINERAMA */

In Ruby, it’s very common pattern

In cpp it uses flags to add or remove features on build. I’m just thinking this is obsolete with a language like Crystal. Since the shards are present in a relative lib directory, we can assume than if we install a specific shard in our project, it’s for using it.

Example

Let’s assume I’ve a shard to upload in a bucket some files. I offer interface for AWS, Google Cloud Storage, and Digital Ocean.

For each of these gateway I’ve a dependent shard I use to connect to theses API.

Currently:

  • When someone is installing my shard, it will install all the dependent shards, for AWS, Google Cloud etc…
  • The source-code will bloat, while the final user want only one environnement, not all the providers.

OR

  • Explain to the final user how to include the dependency for the environment he wants
  • Ask the final user to require both my shard (require "myshard") and the file connecting to gateway he wants (require "myshard/aws")

Now with this feature, the process for the final user would be:

  • If the user want to use a specific gateway without the shard installed, will drop a compile-time raise error which describe what to do.
1 Like

I much prefer the require "myshard/extension" because it’s more explicit.

3 Likes

I just want to note that the "myshard/extension" feature has been discussed before in Impossible to require "extension" shards · Issue #172 · crystal-lang/shards · GitHub

I’m sorry, but we won’t implement this feature. An incompatible solution was chosen long ago, and we don’t plan to change it.

@vladfaust this is about extensions contained within myshard, so that issue isn’t relevant.

1 Like

I always thought this was a really bad design or pattern in Ruby. Then you end up with a general purpose gem that does X but for some reason there’s if defined?(Rails) inside it.

It’s probably better to have a base shard x, and then another shard x-rails that you would require if using Rails.

That said, this works:

class Foo
end

p {{ @type.constants.any? &.==("Foo") }}

So you could use that… but I don’t recommend it.

I been debating how I want to handle optional dependencies, and this kinda relates to it. I wanted to get some other’s thoughts on how to go about it.

Use Case

The current plan for my Athena shard is that athena itself will be made from some required components (like DI/Routing), but also some optional shards (like logging or validation) that are independent and can be used outside of athena itself.

Problem

This setup works just fine with the required shards as I can explicitly add some require within code that integrates that shard into athena core; such as registering services for DI, etc. However, things get a bit more tricky when a shard is not required.

An example would be, say someone installs athena-logger shard. The goal would be by just installing the shard, athena core sets some stuff up in order to integrate the logging component.

Option 1

Have an ext file within the optional shard that should be required by the user in addition to the shard itself

require "athena-logger"
require "athena-logger/ext/athena"

Not a big fan of this approach since this would make the optional shard depend on additional shards that athena core requires, mainly DI.

Option 2

Have an ext file within athena core that should be required by the user if they install the optional shard

require "athena"
require "athena/ext/logger"

This is probably the better more explicit approach

Option 3

Have some file in athena core that auto does the integration if it sees the optional shard is installed

{% if @type.has_constant? "Athena::Logger::Version" %}
  # Do the setup
{% end %}

This would be the most seamless approach as the fact that they installed the other shard says they want to use it within athena core. This could also be used in conjunction with option 2.

I think having a better way to handle option 3 would be a nice to have.

There’s read_file?
not sure if that’s enough :) https://crystal-lang.org/api/0.31.1/Crystal/Macros.html