crystalruby 0.1.12 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41e8370627d23725b55bb1916254b88690e93b384c94692d59d73b0cf5ceda03
4
- data.tar.gz: 7d7192ea627331a30f6cd9ff9dcc7482ed2b1e373f115cc0e3cfa19edd5db506
3
+ metadata.gz: 117525a6b5905a6c4aa86b0b2fed0c25e46d0e97b6de8ce187b90f1438394f19
4
+ data.tar.gz: 55b35d8731c2d7e68f6cb0a1a4ea3eb068805bc4297f1b85fceac08cb288682a
5
5
  SHA512:
6
- metadata.gz: 56d644c482b04bb8f5b39b6626dda93079578f3f95809294a441e0fd1707aeb71eb253da7a549c351fe448750f6dd2acfdbda3782b37e5aaebb4019332c64d0c
7
- data.tar.gz: c529c61b9564f6d23dc551209ac7ce426d56a02424a970b57825f5e993953c376fa57afb3880db0647d581391e7d88881775cd0e2284408997fbaf4e8b37a690
6
+ metadata.gz: 585ad3697b06ba3660989ea538ddae90816d0e93a947e95ab5e6bb83ace64d35988c436cf8e07cad077529691baebbdbdeac6bcc0e7a991297d67f2efada5438
7
+ data.tar.gz: bd06431cc0c059481db8215676e493f16d485cb14a8b15dedc3de6e79df79716c4e6499bb4f9678257659601224b249798dc8c4c492c1659c46157714e45499e
data/.dockerignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ crystalruby-*.gem
10
+ /crystalruby/
data/README.md CHANGED
@@ -43,33 +43,45 @@ E.g.
43
43
  require 'crystalruby'
44
44
  require 'benchmark'
45
45
 
46
- module Fibonnaci
46
+ module PrimeCounter
47
47
  crystalize [n: :int32] => :int32
48
- def fib_cr(n)
49
- a = 0
50
- b = 1
51
- n.times { a, b = b, a + b }
52
- a
48
+ def count_primes_upto_cr(n)
49
+ (2..n).each.count do |i|
50
+ is_prime = true
51
+ (2..Math.sqrt(i).to_i).each do |j|
52
+ if i % j == 0
53
+ is_prime = false
54
+ break
55
+ end
56
+ end
57
+ is_prime
58
+ end
53
59
  end
54
60
 
55
61
  module_function
56
62
 
57
- def fib_rb(n)
58
- a = 0
59
- b = 1
60
- n.times { a, b = b, a + b }
61
- a
63
+ def count_primes_upto_rb(n)
64
+ (2..n).each.count do |i|
65
+ is_prime = true
66
+ (2..Math.sqrt(i).to_i).each do |j|
67
+ if i % j == 0
68
+ is_prime = false
69
+ break
70
+ end
71
+ end
72
+ is_prime
73
+ end
62
74
  end
63
75
  end
64
76
 
65
- puts(Benchmark.realtime { 1_000_000.times { Fibonnaci.fib_rb(30) } })
66
- puts(Benchmark.realtime { 1_000_000.times { Fibonnaci.fib_cr(30) } })
67
-
77
+ include PrimeCounter
78
+ puts(Benchmark.realtime { count_primes_upto_rb(1000_000) })
79
+ puts(Benchmark.realtime { count_primes_upto_cr(1000_000) })
68
80
  ```
69
81
 
70
82
  ```bash
71
- 3.193121999996947 # Ruby
72
- 0.29086600001028273 # Crystal
83
+ 2.8195170001126826 # Ruby
84
+ 0.3402599999681115 # Crystal
73
85
  ```
74
86
 
75
87
  _Note_: The first run of the Crystal code will be slower, as it needs to compile the code first. The subsequent runs will be much faster.
@@ -121,10 +133,10 @@ end
121
133
  ### Crystal Compatible
122
134
 
123
135
  Some Crystal syntax is not valid Ruby, for methods of this form, we need to
124
- define our functions using a :raw parameter.
136
+ define our functions using a raw: true option
125
137
 
126
138
  ```ruby
127
- crystalize :raw, [a: :int, b: :int] => :int
139
+ crystalize [a: :int, b: :int] => :int, raw: true
128
140
  def add(a, b)
129
141
  <<~CRYSTAL
130
142
  c = 0_u64
@@ -453,6 +465,78 @@ CrystalRuby.compile!
453
465
 
454
466
  Then you can run this file as part of your build step, to ensure all Crystal code is compiled ahead of time.
455
467
 
468
+ ## Concurrency
469
+
470
+ While Ruby programs allow multi-threading, Crystal by default uses only a single thread, while utilising Fiber based cooperative-multitasking to allow for concurrent execution. This means that by default, Crystal libraries can not safely be invoked in parallel across multiple Ruby threads.
471
+
472
+ To safely expose this behaviour, `crystalruby` implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread. This way you can safely use `crystalruby` in a multi-threaded Ruby environment.
473
+
474
+ By default `crystalruby` methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor.
475
+
476
+ To allow you to benefit from Crystal's fiber based concurrency, you can use the `async` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
477
+
478
+ E.g.
479
+
480
+ ```ruby
481
+ module Sleeper
482
+ crystalize [] => :void
483
+ def sleep_sync
484
+ sleep 2
485
+ end
486
+
487
+ crystalize [] => :void, async: true
488
+ def sleep_async
489
+ sleep 2
490
+ end
491
+ end
492
+ ```
493
+
494
+ ```ruby
495
+ 5.times.map{ Thread.new{ Sleeper.sleep_sync } }.each(&:join) # Will take 10 seconds
496
+ 5.times.map{ Thread.new{ Sleeper.sleep_async } }.each(&:join) # Will take 2 seconds (the sleeps are processed concurrently)
497
+ ```
498
+
499
+ ### Reactor performance
500
+
501
+ There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing amounts this to be around 10 nanoseconds per call.
502
+ For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2)
503
+ then the overhead of the reactor can become significant.
504
+
505
+ In this case you can use the `crystalruby` in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that _all_ calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.
506
+
507
+ ```ruby
508
+ CrystalRuby.configure do |config|
509
+ config.single_thread_mode = true
510
+ end
511
+ ```
512
+
513
+ ## Live Reloading
514
+
515
+ `crystalruby` supports live reloading of Crystal code. It will intelligently
516
+ recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.
517
+
518
+ ## Multi-library support
519
+
520
+ Large Crystal projects are known to have long compile times. To mitigate this, `crystalruby` supports splitting your Crystal code into multiple libraries. This allows you to only recompile the library that has changed, rather than the entire project.
521
+ To indicate which library a piece of embedded Crystal code belongs to, you can use the `library` option in the `crystalize` and `crystal` methods.
522
+ If the "lib" option is not provided, the code will be compiled into the default library (simply named `crystalruby`).
523
+
524
+ ```ruby
525
+ module Foo
526
+ crystalize lib: "foo"
527
+ def bar
528
+ puts "Hello from Foo"
529
+ end
530
+
531
+ crystal lib: "foo" do
532
+ REDIS = Redis.new
533
+ end
534
+ end
535
+ ```
536
+
537
+ Naturally Crystal code must be in the same library to interact directly.
538
+ Interaction across multiple libraries can be coordinated via Ruby code.
539
+
456
540
  ## Troubleshooting
457
541
 
458
542
  The logic to detect when to JIT recompile is not robust and can end up in an inconsistent state. To remedy this it is useful to clear out all generated assets and build from scratch.
@@ -509,24 +593,24 @@ crystalruby init
509
593
  ```
510
594
 
511
595
  ```yaml
512
- crystal_src_dir: "./crystalruby/src"
513
- crystal_lib_dir: "./crystalruby/lib"
596
+ crystal_src_dir: "./crystalruby"
597
+ crystal_codegen_dir: "generated"
514
598
  crystal_main_file: "main.cr"
515
599
  crystal_lib_name: "crlib"
516
600
  crystal_codegen_dir: "generated"
517
601
  debug: true
518
602
  ```
519
603
 
520
- Alternatively, these can be set programmatically:
604
+ Alternatively, these can be set programmatically, e.g:
521
605
 
522
606
  ```ruby
523
607
  CrystalRuby.configure do |config|
524
- config.crystal_src_dir = "./crystalruby/src"
525
- config.crystal_lib_dir = "./crystalruby/lib"
526
- config.crystal_main_file = "main.cr"
527
- config.crystal_lib_name = "crlib"
608
+ config.crystal_src_dir = "./crystalruby"
528
609
  config.crystal_codegen_dir = "generated"
529
610
  config.debug = true
611
+ config.verbose = false
612
+ config.colorize_log_output = false
613
+ config.log_level = :info
530
614
  end
531
615
  ```
532
616
 
data/exe/crystalruby CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
3
  require "crystalruby"
5
4
  require "fileutils"
6
5
 
@@ -9,11 +8,10 @@ def init
9
8
  # Define some dummy content for the YAML file
10
9
  yaml_content = <<~YAML
11
10
  # crystalruby configuration file
12
- crystal_src_dir: "./crystalruby/src"
13
- crystal_lib_dir: "./crystalruby/lib"
14
- crystal_main_file: "main.cr"
15
- crystal_lib_name: "crlib"
11
+ crystal_src_dir: "./crystalruby"
16
12
  crystal_codegen_dir: "generated"
13
+ log_level: "info"
14
+ single_thread_mode: false
17
15
  debug: true
18
16
  YAML
19
17
 
@@ -23,23 +21,25 @@ def init
23
21
  end
24
22
 
25
23
  def install
26
- Dir.chdir("#{CrystalRuby.config.crystal_src_dir}") do
27
- if system("shards update")
28
- puts "Shards installed successfully."
29
- else
30
- puts "Error installing shards."
24
+ Dir["#{CrystalRuby.config.crystal_src_dir}/**/src"].each do |src_dir|
25
+ Dir.chdir(src_dir) do
26
+ if system("shards check") || system("shards update")
27
+ puts "Shards installed successfully."
28
+ else
29
+ puts "Error installing shards."
30
+ end
31
31
  end
32
32
  end
33
- clean
34
33
  end
35
34
 
36
35
  def clean
37
- # This is a stub for the clear command
38
- FileUtils.rm_rf("#{CrystalRuby.config.crystal_src_dir}/generated")
36
+ Dir["#{CrystalRuby.config.crystal_src_dir}/**/src/generated"].each do |codegen_dir|
37
+ FileUtils.rm_rf(codegen_dir)
38
+ end
39
39
  end
40
40
 
41
41
  def build
42
- # This is a stub for the build command
42
+ # TODO: Iterate through all generated libs and build
43
43
  puts "Build command is not implemented yet."
44
44
  end
45
45
 
@@ -0,0 +1,133 @@
1
+ module CrystalRuby
2
+ module Adapter
3
+ # Use this method to annotate a Ruby method that should be crystalized.
4
+ # Compilation and attachment of the method is done lazily.
5
+ # You can force compilation by calling `CrystalRuby.compile!`
6
+ # It's important that all code using crystalized methods is
7
+ # loaded before any manual calls to compile.
8
+ #
9
+ # E.g.
10
+ #
11
+ # crystalize [a: :int32, b: :int32] => :int32
12
+ # def add(a, b)
13
+ # a + b
14
+ # end
15
+ #
16
+ # Pass `raw: true` to pass Raw crystal code to the compiler as a string instead.
17
+ # (Useful for cases where the Crystal method body is not valid Ruby)
18
+ # E.g.
19
+ # crystalize raw: true [a: :int32, b: :int32] => :int32
20
+ # def add(a, b)
21
+ # <<~CRYSTAL
22
+ # a + b
23
+ # CRYSTAL
24
+ # end
25
+ #
26
+ # Pass `async: true` to make the method async.
27
+ # Crystal methods will always block the currently executing Ruby thread.
28
+ # With async: false, all other Crystal code will be blocked while this Crystal method is executing (similar to Ruby code with the GVL)
29
+ # With async: true, several Crystal methods can be executing concurrently.
30
+ #
31
+ # Pass lib: "name_of_lib" to compile Crystal code into several distinct libraries.
32
+ # This can help keep compilation times low, by packaging your Crystal code into separate shared objects.
33
+ def crystalize(raw: false, async: false, lib: "crystalruby", **options, &block)
34
+ (args,), returns = options.first || [[], :void]
35
+ args ||= {}
36
+ raise "Arguments should be of the form name: :type. Got #{args}" unless args.is_a?(Hash)
37
+
38
+ @crystalize_next = {
39
+ raw: raw,
40
+ async: async,
41
+ args: args,
42
+ returns: returns,
43
+ block: block,
44
+ lib: lib
45
+ }
46
+ end
47
+
48
+ # This method provides a useful DSL for defining Crystal types in pure Ruby.
49
+ # These types can not be passed directly to Ruby, and must be serialized as either:
50
+ # JSON or
51
+ # C-Structures (WIP)
52
+ #
53
+ # See #json for an example of how to define arguments or return types for complex objects.
54
+ # E.g.
55
+ #
56
+ # MyType = crtype{ Int32 | Hash(String, Array(Bool) | Float65 | Nil) }
57
+ def crtype(&block)
58
+ TypeBuilder.with_injected_type_dsl(self) do
59
+ TypeBuilder.build(&block)
60
+ end
61
+ end
62
+
63
+ # Use the json{} helper for defining complex method arguments or return types
64
+ # that should be serialized to and from Crystal using JSON. (This conversion is applied automatically)
65
+ #
66
+ # E.g.
67
+ # crystalize [a: json{ Int32 | Float64 | Nil } ] => NamedStruct(result: Int32 | Float64 | Nil)
68
+ def json(&block)
69
+ crtype(&block).serialize_as(:json)
70
+ end
71
+
72
+ # We trigger attaching of crystalized instance methods here.
73
+ # If a method is added after a crystalize annotation we assume it's the target of the crystalize annotation.
74
+ def method_added(method_name)
75
+ define_crystalized_method(method_name, instance_method(method_name)) if @crystalize_next
76
+ super
77
+ end
78
+
79
+ # We trigger attaching of crystalized class methods here.
80
+ # If a method is added after a crystalize annotation we assume it's the target of the crystalize annotation.
81
+ def singleton_method_added(method_name)
82
+ define_crystalized_method(method_name, singleton_method(method_name)) if @crystalize_next
83
+ super
84
+ end
85
+
86
+ # Use this method to define inline Crystal code that does not need to be bound to a Ruby method.
87
+ # This is useful for defining classes, modules, performing set-up tasks etc.
88
+ # See: docs for .crystalize to understand the `raw` and `lib` parameters.
89
+ def crystal(raw: false, lib: "crystalruby", &block)
90
+ inline_crystal_body = Template::InlineChunk.render(
91
+ {
92
+ module_name: name, body: extract_source(block, raw: raw)
93
+ }
94
+ )
95
+ CrystalRuby::Library[lib].crystalize_chunk(
96
+ self,
97
+ Digest::MD5.hexdigest(inline_crystal_body),
98
+ inline_crystal_body
99
+ )
100
+ end
101
+
102
+ # We attach crystalized class methods here.
103
+ # This function is responsible for
104
+ # - Generating the Crystal source code
105
+ # - Overwriting the method and class methods by the same name in the caller.
106
+ # - Lazily triggering compilation and attachment of the Ruby method to the Crystal code.
107
+ # - We also optionally prepend a block (if given) to the owner, to allow Ruby code to wrap around Crystal code.
108
+ def define_crystalized_method(method_name, method)
109
+ CrystalRuby.log_debug("Defining crystalized method #{name}.#{method_name}")
110
+
111
+ args, returns, block, async, lib, raw = @crystalize_next.values_at(:args, :returns, :block, :async, :lib, :raw)
112
+ @crystalize_next = nil
113
+
114
+ CrystalRuby::Library[lib].crystalize_method(
115
+ method,
116
+ args,
117
+ returns,
118
+ extract_source(method, raw: raw),
119
+ async,
120
+ &block
121
+ )
122
+ end
123
+
124
+ # Extract Ruby source to serve as Crystal code directly.
125
+ # If it's a raw method, we'll strip the string delimiters at either end of the definition.
126
+ # We need to clear the MethodSource cache here to allow for code reloading.
127
+ def extract_source(method_or_block, raw: false)
128
+ method_or_block.source.lines[raw ? 2...-2 : 1...-1].join("\n").tap do
129
+ MethodSource.instance_variable_get(:@lines_for_file).delete(method_or_block.source_location[0])
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,75 +1,28 @@
1
1
  require "open3"
2
2
  require "tmpdir"
3
+ require "shellwords"
3
4
 
4
5
  module CrystalRuby
5
6
  module Compilation
7
+ class CompilationFailedError < StandardError; end
8
+
6
9
  def self.compile!(
7
- src: config.crystal_src_dir_abs / config.crystal_main_file,
8
- lib: config.crystal_lib_dir_abs / config.crystal_lib_name,
9
- verbose: config.verbose,
10
- debug: config.debug
10
+ src:,
11
+ lib:,
12
+ verbose: CrystalRuby.config.verbose,
13
+ debug: CrystalRuby.config.debug
11
14
  )
12
- Dir.chdir(config.crystal_src_dir_abs) do
13
- compile_command = compile_command!(verbose: verbose, debug: debug, lib: lib, src: src)
14
- link_command = link_cmd!(verbose: verbose, lib: lib, src: src)
15
-
16
- puts "[crystalruby] Compiling Crystal code: #{compile_command}" if verbose
17
- unless system(compile_command)
18
- puts "Failed to build Crystal object file."
19
- return false
20
- end
21
-
22
- puts "[crystalruby] Linking Crystal code: #{link_command}" if verbose
23
- unless system(link_command)
24
- puts "Failed to link Crystal library."
25
- return false
26
- end
27
- end
28
-
29
- true
15
+ compile_command = build_compile_command(verbose: verbose, debug: debug, lib: lib, src: src)
16
+ CrystalRuby.log_debug "Compiling Crystal code #{verbose ? ": #{compile_command}" : ""}"
17
+ raise CompilationFailedError, "Compilation failed" unless system(compile_command)
30
18
  end
31
19
 
32
- def self.compile_command!(verbose:, debug:, lib:, src:)
33
- @compile_command ||= begin
34
- verbose_flag = verbose ? "--verbose" : ""
35
- debug_flag = debug ? "" : "--release --no-debug"
36
- redirect_output = " > /dev/null " unless verbose
37
-
38
- %(crystal build #{verbose_flag} #{debug_flag} --cross-compile -o #{lib} #{src}#{redirect_output})
39
- end
40
- end
41
-
42
- # Here we misuse the crystal compiler to build a valid linking command
43
- # with all of the platform specific flags that we need.
44
- # We then use this command to link the object file that we compiled in the previous step.
45
- # This is not robust and is likely to need revision in the future.
46
- def self.link_cmd!(verbose:, lib:, src:)
47
- @link_cmd ||= begin
48
- result = nil
49
-
50
- Dir.mktmpdir do |tmp|
51
- output, status = Open3.capture2("crystal build --verbose #{src} -o #{Pathname.new(tmp) / "main"}")
52
- unless status.success?
53
- puts "Failed to compile the Crystal code."
54
- exit 1
55
- end
56
-
57
- # Parse the output to find the last invocation of the C compiler, which is likely the linking stage
58
- # and strip off the targets that the crystal compiler added.
59
- link_command_suffix = output.lines.select { |line| line.strip.start_with?("cc") }.last.strip[/.*(-o.*)/, 1]
60
-
61
- # Replace the output file with the path to the object file we compiled
62
- link_command_suffix.gsub!(
63
- /-o.*main/,
64
- "-o #{lib}"
65
- )
66
- result = %(cc #{lib}.o -shared #{link_command_suffix})
67
- result << " > /dev/null 2>&1" unless verbose
68
- result
69
- end
70
-
71
- result
72
- end
20
+ def self.build_compile_command(verbose:, debug:, lib:, src:)
21
+ verbose_flag = verbose ? "--verbose --progress" : ""
22
+ debug_flag = debug ? "" : "--release --no-debug"
23
+ redirect_output = " &> /dev/null " unless verbose
24
+ lib, src, lib_dir = [lib, src, File.dirname(src)].map(&Shellwords.method(:escape))
25
+ %(cd #{lib_dir} && crystal build #{verbose_flag} #{debug_flag} --single-module --link-flags "-shared" -o #{lib} #{src}#{redirect_output})
73
26
  end
74
27
  end
75
28
  end
@@ -1,15 +1,23 @@
1
1
  require "singleton"
2
2
  require "yaml"
3
+ require "logger"
3
4
 
4
5
  module CrystalRuby
5
- def self.config
6
- Config.instance
6
+ # Config mixin to easily access the configuration
7
+ # from anywhere in the code
8
+ module Config
9
+ def config
10
+ Configuration.instance
11
+ end
7
12
  end
8
13
 
9
- # Define a nested Config class
10
- class Config
14
+ # Defines our configuration singleton
15
+ # Config can be specified through either:
16
+ # - crystalruby.yaml OR
17
+ # - CrystalRuby.configure block
18
+ class Configuration
11
19
  include Singleton
12
- attr_accessor :debug, :verbose
20
+ attr_accessor :debug, :verbose, :logger, :colorize_log_output, :single_thread_mode
13
21
 
14
22
  def initialize
15
23
  @debug = true
@@ -19,17 +27,19 @@ module CrystalRuby
19
27
  rescue StandardError
20
28
  nil
21
29
  end || {}
22
- @crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby/src")
23
- @crystal_lib_dir = config.fetch("crystal_lib_dir", "./crystalruby/lib")
24
- @crystal_main_file = config.fetch("crystal_main_file", "main.cr")
25
- @crystal_lib_name = config.fetch("crystal_lib_name", "crlib")
30
+ @crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby")
26
31
  @crystal_codegen_dir = config.fetch("crystal_codegen_dir", "generated")
27
32
  @crystal_project_root = config.fetch("crystal_project_root", Pathname.pwd)
28
33
  @debug = config.fetch("debug", true)
29
34
  @verbose = config.fetch("verbose", false)
35
+ @single_thread_mode = config.fetch("single_thread_mode", false)
36
+ @colorize_log_output = config.fetch("colorize_log_output", false)
37
+ @log_level = config.fetch("log_level", ENV.fetch("CRYSTALRUBY_LOG_LEVEL", "info"))
38
+ @logger = Logger.new(STDOUT)
39
+ @logger.level = Logger.const_get(@log_level.to_s.upcase)
30
40
  end
31
41
 
32
- %w[crystal_main_file crystal_lib_name crystal_project_root].each do |method_name|
42
+ %w[crystal_project_root].each do |method_name|
33
43
  define_method(method_name) do
34
44
  @paths_cache[method_name] ||= Pathname.new(instance_variable_get(:"@#{method_name}"))
35
45
  end
@@ -46,7 +56,7 @@ module CrystalRuby
46
56
  end
47
57
  end
48
58
 
49
- %w[crystal_src_dir crystal_lib_dir].each do |method_name|
59
+ %w[crystal_src_dir].each do |method_name|
50
60
  abs_method_name = "#{method_name}_abs"
51
61
  define_method(abs_method_name) do
52
62
  @paths_cache[abs_method_name] ||= crystal_project_root / instance_variable_get(:"@#{method_name}")
@@ -56,8 +66,14 @@ module CrystalRuby
56
66
  @paths_cache[method_name] ||= Pathname.new instance_variable_get(:"@#{method_name}")
57
67
  end
58
68
  end
69
+
70
+ def log_level=(level)
71
+ @log_level = level
72
+ @logger.level = Logger.const_get(level.to_s.upcase)
73
+ end
59
74
  end
60
75
 
76
+ extend Config
61
77
  def self.configure
62
78
  yield(config)
63
79
  @paths_cache = {}