coconut 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -19,16 +19,16 @@ Or install it yourself as:
19
19
  $ gem install coconut
20
20
 
21
21
 
22
- ## What is coconut?
22
+ ## Overview
23
23
  Coconut provides **a simple DSL that allows you to configure your application's
24
- assets on different environments**
24
+ assets on different environments**.
25
25
 
26
26
  ```ruby
27
27
  require 'coconut'
28
28
 
29
29
  Coconut.configure MyApp do
30
30
  twitter do
31
- environment :development do
31
+ environments :development, :test do
32
32
  consumer_key 'development_key'
33
33
  consumer_secret 'development_secret'
34
34
  end
@@ -41,11 +41,8 @@ Coconut.configure MyApp do
41
41
  end
42
42
  ```
43
43
 
44
- You only need to require file from your app. Coconut will define a
44
+ You only need to require the config file from your app. Coconut will define a
45
45
  `config` method on your application's namespace that you can use to query it.
46
- **You don't have to specify the environment when querying for configuration
47
- values**. Coconut will only run the configuration for the environment it's
48
- running on.
49
46
 
50
47
  ```ruby
51
48
  ENV['RACK_ENV'] = :development
@@ -55,28 +52,244 @@ ENV['RACK_ENV'] = :production
55
52
  MyApp.config.twitter.consumer_key # => production_key
56
53
  ```
57
54
 
58
- ## Why coconut?
59
- TODO
55
+ **You don't have to specify the environment when querying for configuration
56
+ values. Coconut will only run the configuration for the environment it's
57
+ running on.**
60
58
 
61
59
 
62
- ## Coconut flavours
63
- TODO
64
- ### Single file
65
- TODO
60
+ ## How is the environment detected?
61
+ Coconut uses the `RACK_ENV` environment variable by default to detect the
62
+ environment the app is running on. If this variable is not set or is empty it
63
+ will default to `:development`.
66
64
 
67
- ### Folder
68
- TODO
65
+ If your ecosystem uses something that is not RACK based you can specify how
66
+ Coconut should find out the environment with the `environment` method on the
67
+ **Application** level
68
+ (See the *Specifying how the environment should be found* section on
69
+ *Application* under *Coconut Anatomy*).
69
70
 
70
- ### List of files
71
- TODO
72
71
 
72
+ ## Coconut Anatomy
73
+ Coconut is composed of **3 different parts**:
73
74
 
74
- ## How is the environment detected?
75
- Right now coconut uses the environment variable `RACK_ENV` to detect the
76
- environment the app is running on. If `RACK_ENV` is not set or is empty it
77
- defaults to `:development`.
75
+ ### 1) Application
76
+ *Application is the top level block* of Coconut. It's composed of the configuration
77
+ of the *Assets* your application has. It may also contain information about
78
+ where Coconut should look for *Asset* configuration files and how it should
79
+ find out which environment the application is running on.
80
+
81
+ You will enter this block when you call the `Coconut.configure` method passing
82
+ in your application's namespace.
83
+
84
+ ```ruby
85
+ require 'coconut'
86
+
87
+ Coconut.configure MyApp do
88
+ # your application configuration
89
+ end
90
+ ```
91
+
92
+ Coconut will then detect the environment,
93
+ run the configuration and create a `Coconut::Config` object with the assets
94
+ and it's properties for the current environment. A `config` method will be
95
+ defined in the namespace passed to `configure`. That method will return the
96
+ `Coconut::Config` object with your application's configuration.
97
+
98
+ That means:
99
+ * You only need to make sure that you require the config file that includes the
100
+ *Application* block for your configuration to be run.
101
+ * Your configuration will be run just once (unless you use `Kernel::load`)
102
+ * If there is a `config` method on your namespace it will be overriden.
103
+ * You will be able to access the configuration from anywhere by calling
104
+ `Namespace::config`.
105
+
106
+ #### Specifying how the environment should be found
107
+ If your application should not use the RACK_ENV environment variable to
108
+ determine which configuration to use you can tell Coconut how to find out:
109
+
110
+ ```ruby
111
+ Coconut.configure MyApp do
112
+ environment { MyApp::find_out_environment }
113
+ # ...
114
+ end
115
+ ```
116
+
117
+ Coconut will call the block passed to the `environment` method to find out
118
+ what environment the application is running on. Therefore **the environment
119
+ will be the return value of that block**.
120
+
121
+
122
+ #### Coconut flavours (a.k.a. how to structure your configuration).
123
+ You can structure the configuration the way that suits you best. Maybe it fits
124
+ in a single file best. Maybe you want to split it and have several configuration
125
+ files. Coconut offers you three different alternatives.
126
+
127
+ #####a) Single file
128
+ If your configuration is small this is probably the best choice. In this case
129
+ your application section will only be composed of *Assets*. You will need
130
+ nothing more. The first example of this README uses this Coconut flavour.
131
+
132
+ #####b) Folder
133
+ If you want to split your configuration in several files and put them in the
134
+ same folder you will be able to tell Coconut how to find them. Coconut will
135
+ load **every file in that folder and run them as if they were Coconut scripts**.
136
+
137
+ If the file that has the *Application* level configuration is in that folder
138
+ too it will be run and will cause an error. You have two options to avoid this:
139
+
140
+ 1. Name that file `config.rb`. By convention Coconut won't run a file in an
141
+ asset folder if it is called like that.
142
+ 2. Put the configuration file that has the *Application* level configuration
143
+ in a different folder. In this case the files in the given folder will only
144
+ have *Asset* configuration.
145
+
146
+ ```ruby
147
+ Coconut.configure MyApp do
148
+ asset_folder 'path/to/folder'
149
+ end
150
+ ```
151
+
152
+ This allows you to write your files focusing only on *Assets*, given that
153
+ when Coconut will run then within the *Application* block. So you could
154
+ have the following project structure:
155
+
156
+ .
157
+ ..
158
+ /config
159
+ config.rb
160
+ database.rb
161
+ oauth.rb
162
+ s3.rb
163
+ /lib
164
+ /spec
165
+
166
+ You could have the *Application* block on the `config.rb` file like this
167
+ (assuming that you run the application from the root of the project):
168
+
169
+ ```ruby
170
+ Coconut.configure MyApp do
171
+ asset_folder 'config'
172
+ end
173
+ ```
174
+
175
+ And each of the asset files would only include asset configuration:
78
176
 
79
- ### Changing the environment
177
+ ```ruby
178
+ database do
179
+ environment :development do
180
+ #...
181
+ end
182
+
183
+ environment :staging, :production do
184
+ #...
185
+ end
186
+ end
187
+ ```
188
+
189
+ You would only need to require the `config/config.rb` file to access the
190
+ configuration.
191
+
192
+ #####c) List of files
193
+
194
+ *NOT DEVELOPED YET: will be available on next version (0.1.1)*
195
+
196
+ This flavour is exactly like the *folder* one with the difference that instead
197
+ of providing the path to the folder to the asset configuration files you provide
198
+ the path to every one of them:
199
+
200
+ ```ruby
201
+ Coconut.configure MyApp do
202
+ asset_files 'database.rb', 'oauth.rb', 's3.rb'
203
+ end
204
+ ```
205
+
206
+ Notice that the paths are relative to where the `config.rb` file is located.
207
+
208
+ ### 2) Asset
209
+ Each of this blocks represent one of the assets of your application. Inside
210
+ each one of them you will include the *Asset* properties on each of the
211
+ environments your application will run on.
212
+
213
+ An *Asset* block consist of the asset name and a Ruby block containing it's
214
+ configuration in each of the environments:
215
+
216
+ ```ruby
217
+ asset_name do
218
+ environment(:production) { property 'value on production' }
219
+ environment(:development, :test) { property 'value on test and development' }
220
+ end
221
+ ```
222
+
223
+ You can specify one or more environments at the same time. You can also use any
224
+ of the aliases the `environment` method has:
225
+
226
+ ```ruby
227
+ env(:development, :test) { property 'value' }
228
+ envs(:development, :test) { property 'value' }
229
+ environment(:development, :test) { property 'value' }
230
+ environments(:development, :test) { property 'value' }
231
+ ```
232
+
233
+ An *Asset* can have *almost* any name. There are some restrictions that apply to
234
+ the names. This restrictions apply to both asset names and property names
235
+ and are explained in the *Environment* section of this README.
236
+
237
+ ### 3) Environment
238
+ Environments are the lowest level of a Coconut configuration. Its composed of
239
+ the configuration properties an asset will have on that environment. So,
240
+ essentially, it's a series of key-value pairs.
241
+
242
+ Properties can have almost any name you can think of. It only has to be a valid
243
+ Ruby method name and you have to avoid those that will collide with calls on
244
+ the `Config` object. Coconut will raise an error if you use one of the reserved
245
+ names:
246
+
247
+ ```ruby
248
+ environment(:development) { object_id 11 }
249
+ # => Coconut::Dsl::InvalidName: object_id can't be used as property name: it will collide with Coconut::Config methods
250
+ ```
251
+
252
+ You can find out the forbidden property (and asset) names by calling the method:
253
+ ```ruby
254
+ Coconut::Dsl::BlankSlate.__forbidden_names
255
+ # => [:respond_to?, :to_hash, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup,
256
+ # :initialize_dup, :initialize_clone, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze,
257
+ # :frozen?, :to_s, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods,
258
+ # :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set,
259
+ # :instance_variable_defined?, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send,
260
+ # :respond_to_missing?, :extend, :display, :method, :public_method, :define_singleton_method, :object_id,
261
+ # :to_enum, :enum_for, :==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__,
262
+ # :instance_eval, :__send__, :object_id, :__taken?, :__taken_error_message]
263
+ ```
264
+
265
+ If you want to use the methods from `Kernel` that are widely available
266
+ on Ruby objects you will need to prepend the module to the method call:
267
+
268
+ ```ruby
269
+ environment(:development) do
270
+ puts 'here' # declaring a property named "puts"
271
+ Kernel::puts 'here' # printing "here" to STDOUT
272
+ end
273
+ ```
274
+
275
+ Values can be any Ruby object, from a simple integer or string to a lambda.
276
+ You can even use constant or expressions. Keep in mind that the block passed
277
+ to environment will be evaluated in a different context:
278
+ you won't be able to call methods without specifying their receiver.
279
+
280
+ ```ruby
281
+ environment :development do
282
+ four 2 + 2
283
+ arguments ARGV
284
+ encodings Encoding.list
285
+ setup -> do
286
+ enable :sessions, :logging
287
+ disable :dump_errors, :some_custom_option
288
+ end
289
+ end
290
+ ```
291
+
292
+ ## Why coconut?
80
293
  TODO
81
294
 
82
295
  ## Contributing
data/Rakefile CHANGED
@@ -1 +1,30 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'cucumber/rake/task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ namespace :cucumber do
6
+ Cucumber::Rake::Task.new(:ok, 'Run features that should pass') do |t|
7
+ t.cucumber_opts = ['--strict', '--tags ~@wip', '--format progress']
8
+ end
9
+
10
+ Cucumber::Rake::Task.new(:wip, 'Run features that are being worked on') do |t|
11
+ t.cucumber_opts = ['--wip', '--tags @wip']
12
+ end
13
+
14
+ Cucumber::Rake::Task.new(:all, 'Run all features')
15
+ end
16
+
17
+ namespace :rspec do
18
+ desc 'Run specs with progress output'
19
+ RSpec::Core::RakeTask.new(:progress) do |t|
20
+ t.rspec_opts = '--format progress'
21
+ end
22
+
23
+ desc 'Run all specs'
24
+ RSpec::Core::RakeTask.new(:spec)
25
+ end
26
+
27
+ task spec: 'rspec:spec'
28
+ task cucumber: 'cucumber:ok'
29
+ task test: ['rspec:progress', 'cucumber:ok']
30
+ task default: 'test'
@@ -0,0 +1,8 @@
1
+ # Coconut release changes
2
+
3
+ ## Version 0.0.1
4
+ - Single file configuration preview.
5
+
6
+ ## Version 0.1.0
7
+ - Remove Hashie dependency.
8
+ - Add *folder flavour* support.
@@ -17,9 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_runtime_dependency 'hashie'
21
-
22
20
  gem.add_development_dependency 'cucumber'
23
21
  gem.add_development_dependency 'rake'
24
22
  gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'yard'
25
24
  end
@@ -0,0 +1,52 @@
1
+ Feature: Load configuration from folder
2
+
3
+ You can split your configuration into asset files, put those files into a
4
+ folder and tell Coconut to load the configuration from there. That
5
+ folder should only include asset files. If it includes some other Ruby file
6
+ Coconut will try to execute it as such and you will get an error.
7
+
8
+ In order to be able to have all the configuration related files in a single
9
+ folder you can adhere to the convention and put your application configuration
10
+ in a file called 'config.rb' inside that folder.
11
+
12
+ If Coconut will ignore that file and it won't be executed as if it was an
13
+ asset file.
14
+
15
+ You can also have your application related configuration in other folder and
16
+ use the file name that you want.
17
+
18
+ Scenario: Application file and assets file under the same folder
19
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
20
+ """
21
+ Coconut.configure MyApp do
22
+ asset_folder '/tmp/coconut_config'
23
+ end
24
+ """
25
+ And I have a "s3.rb" asset file on /tmp/coconut_config with content:
26
+ """
27
+ s3 do
28
+ env(:development) { key 'xxx' }
29
+ env(:production) { key 'yyy' }
30
+ end
31
+ """
32
+ When I run my application on the "development" environment
33
+ And I query the configuration for "s3.key"
34
+ Then the configured value should be "xxx"
35
+
36
+ Scenario: Application file and assets file in different folders
37
+ Given I have my application config in "/tmp/coconut.rb" with content:
38
+ """
39
+ Coconut.configure MyApp do
40
+ asset_folder '/tmp/coconut_config'
41
+ end
42
+ """
43
+ And I have a "s3.rb" asset file on /tmp/coconut_config with content:
44
+ """
45
+ facebook do
46
+ env(:development) { key 'xxx' }
47
+ env(:production) { key 'yyy' }
48
+ end
49
+ """
50
+ When I run my application on the "development" environment
51
+ And I query the configuration for "facebook.key"
52
+ Then the configured value should be "xxx"
@@ -1,7 +1,7 @@
1
1
  Feature: Single file configuration
2
2
 
3
3
  Scenario: One environment configuration
4
- Given my application is configured like this:
4
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
5
5
  """
6
6
  Coconut.configure(MyApp) do
7
7
  ftp do
@@ -14,3 +14,18 @@ Feature: Single file configuration
14
14
  When I run my application on the "development" environment
15
15
  And I query the configuration for "ftp.user"
16
16
  Then the configured value should be "root"
17
+
18
+ Scenario: Several environments configuration
19
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
20
+ """
21
+ Coconut.configure(MyApp) do
22
+ ssh do
23
+ environment(:development, :staging, :production) do
24
+ login 'username'
25
+ end
26
+ end
27
+ end
28
+ """
29
+ When I run my application on the "development" environment
30
+ And I query the configuration for "ssh.login"
31
+ Then the configured value should be "username"
@@ -1,10 +1,15 @@
1
- Given /^my application is configured like this:$/ do |configuration|
2
- @app_config = configuration
1
+ Given /^I have my application config in "(.*?)" with content:$/ do |file, content|
2
+ @config = file
3
+ File.open(file, 'w+') { |file| file.write(content) }
4
+ end
5
+
6
+ Given /^I have a "(.*?)" asset file on (.*?) with content:$/ do |name, folder, content|
7
+ File.open(File.join(folder, name), 'w+') { |file| file.write(content) }
3
8
  end
4
9
 
5
10
  When /^I run my application on the "(.*?)" environment$/ do |environment|
6
11
  ENV['RACK_ENV']= environment
7
- eval @app_config
12
+ eval "load '#{@config}'"
8
13
  end
9
14
 
10
15
  When /^I query the configuration for "(.*?)"$/ do |query|
@@ -2,4 +2,32 @@ $: << File.join(File.dirname(__FILE__), '..', '..', 'lib')
2
2
 
3
3
  require 'coconut'
4
4
 
5
- module MyApp; end
5
+ CONFIG_FOLDER = '/tmp/coconut_config/'
6
+
7
+ Before do
8
+ clean_tmp_folder
9
+ create_folder_for_testing
10
+ create_namespace_for_testing
11
+ end
12
+
13
+ After do
14
+ clean_tmp_folder
15
+ clean_namespace_for_testing
16
+ end
17
+
18
+ def create_namespace_for_testing
19
+ Object.send(:const_set, :MyApp, Module.new)
20
+ end
21
+
22
+ def clean_namespace_for_testing
23
+ Object.send(:remove_const, :MyApp)
24
+ end
25
+
26
+ def clean_tmp_folder
27
+ `rm -rf #{CONFIG_FOLDER}` if File.directory?(CONFIG_FOLDER)
28
+ `rm -f /tmp/coconut.rb` if File.exists?('/tmp/coconut.rb')
29
+ end
30
+
31
+ def create_folder_for_testing
32
+ `mkdir #{CONFIG_FOLDER}`
33
+ end
@@ -1,27 +1,27 @@
1
1
  require_relative 'coconut/version'
2
- require_relative 'coconut/config'
2
+ require_relative 'coconut/dsl/application'
3
3
 
4
4
  module Coconut
5
- def self.configure(namespace, &assets)
6
- raise "#{namespace} already has a config method" if namespace.respond_to? :config
7
- define_config_method namespace, Config.new(current_environment, &assets)
5
+ include Dsl
6
+
7
+ def self.configure(namespace, &config)
8
+ define_config_method namespace, Application.configure(environment, &config)
8
9
  end
9
10
 
10
- def self.current_environment
11
+ def self.environment
12
+ return @__coconut_environment.() unless @__coconut_environment.nil?
11
13
  ENV['RACK_ENV'] || :development
12
14
  end
13
15
 
16
+ def self.take_environment_from(&block)
17
+ @__coconut_environment = block
18
+ end
19
+
14
20
  private
15
21
 
16
22
  def self.define_config_method(namespace, configuration)
17
- singleton_class_of(namespace).instance_eval do
18
- define_method :config do
19
- @_coconut_configuration ||= configuration
20
- end
23
+ namespace.singleton_class.instance_eval do
24
+ define_method(:config) { @__coconut_configuration ||= configuration }
21
25
  end
22
26
  end
23
-
24
- def self.singleton_class_of(class_or_module)
25
- class << class_or_module; self; end
26
- end
27
27
  end
@@ -1,23 +1,53 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'asset'
4
- require_relative 'runner'
5
-
6
1
  module Coconut
7
- class Config < DelegateClass(Hashie::Mash)
8
- def initialize(current_environment, &assets_config)
9
- super Hashie::Mash.new
10
- run assets_config, current_environment
2
+ class Config
3
+ def initialize(properties)
4
+ @properties = properties
5
+ @config = config_from(properties)
6
+ end
7
+
8
+ # Coconut::Config objects will respond to methods if its name is
9
+ # equal to any of the configured properties. This method is redefined to
10
+ # make its behaviour consistent with Coconut::Config#method_missing
11
+ #
12
+ # @return [Boolean] whether a config object knows how to respond to method "name"
13
+ def respond_to?(name)
14
+ property?(name) or super
15
+ end
16
+
17
+ # @return [Hash] a Hash representation of the configuration object
18
+ def to_hash
19
+ @properties.dup
11
20
  end
12
21
 
13
22
  private
14
23
 
15
- def run(config, environment)
16
- Runner.new run: config, method_missing: configure_asset_on(environment)
24
+ def config_from(properties)
25
+ properties.each_with_object({}) do |(property, value), config|
26
+ config[property]= config_value_for(value)
27
+ end
28
+ end
29
+
30
+ def config_value_for(value)
31
+ return Config.new(value) if value.is_a? Hash
32
+ value
33
+ end
34
+
35
+ def method_missing(name, *args, &block)
36
+ if property?(name)
37
+ define_property_accessor(name)
38
+ return @config[name]
39
+ end
40
+ super
41
+ end
42
+
43
+ def property?(name)
44
+ @config.has_key?(name)
17
45
  end
18
46
 
19
- def configure_asset_on(environment)
20
- ->(name, *args, &config){ self[name] = Asset.new(environment, &config) }
47
+ def define_property_accessor(name)
48
+ singleton_class.instance_eval do
49
+ define_method(name) { @config[name] }
50
+ end
21
51
  end
22
52
  end
23
53
  end
@@ -0,0 +1,35 @@
1
+ require_relative 'asset'
2
+ require_relative 'asset_folder'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Application < BlankSlate
7
+ def self.configure(current_environment, &config)
8
+ new(current_environment).run(&config)
9
+ end
10
+
11
+ def initialize(current_environment)
12
+ @current_environment = current_environment
13
+ end
14
+
15
+ def run(&config)
16
+ @assets_config = {}
17
+ instance_eval &config
18
+ Config.new(@assets_config)
19
+ end
20
+
21
+ private
22
+
23
+ def asset_folder(path)
24
+ instance_eval AssetFolder.config_from(path, IGNORED_FILES)
25
+ end
26
+
27
+ def method_missing(asset, *args, &config)
28
+ ::Kernel::raise InvalidName, __taken_error_message(asset, 'asset name') if __taken?(asset)
29
+ @assets_config[asset] = Asset.configure(@current_environment, &config)
30
+ end
31
+
32
+ IGNORED_FILES = ['config.rb']
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../config'
2
+ require_relative './environment'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Asset
7
+ def self.configure(environment, &config)
8
+ new(environment).run(&config)
9
+ end
10
+
11
+ def initialize(current_environment)
12
+ @current_environment = current_environment
13
+ end
14
+
15
+ def run(&config)
16
+ @properties = {}
17
+ instance_eval &config
18
+ @properties
19
+ end
20
+
21
+ private
22
+
23
+ def environment(*environments, &config)
24
+ environments.each { |environment| __configure(environment, config) }
25
+ end
26
+
27
+ alias :env :environment
28
+ alias :envs :environment
29
+ alias :environments :environment
30
+
31
+ def __configure(environment, config)
32
+ @properties.merge! Environment.configure(&config) if __current?(environment)
33
+ end
34
+
35
+ def __current?(environment)
36
+ @current_environment.to_sym == environment.to_sym
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module Coconut
2
+ module Dsl
3
+ class AssetFolder
4
+ def self.config_from(path, ignored_files)
5
+ new(path, ignored_files).assets_config
6
+ end
7
+
8
+ def initialize(path, ignored_files)
9
+ @path = path
10
+ @ignored_files = ignored_files.map(&method(:path_to))
11
+ end
12
+
13
+ def assets_config
14
+ asset_files_in_folder.map { |file| content(file) }.join "\n"
15
+ end
16
+
17
+ private
18
+
19
+ def asset_files_in_folder
20
+ ruby_files_in_folder - @ignored_files
21
+ end
22
+
23
+ def ruby_files_in_folder
24
+ files_in_folder.select { |file| file.match /\.rb$/ }
25
+ end
26
+
27
+ def files_in_folder
28
+ folder.entries.map(&method(:path_to)).reject { |path| File.directory? path }
29
+ end
30
+
31
+ def folder
32
+ Dir.open(@path)
33
+ end
34
+
35
+ def content(file)
36
+ File.read(file)
37
+ end
38
+
39
+ def path_to(file)
40
+ File.expand_path(File.join(@path, file))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../config'
2
+
3
+ module Coconut
4
+ module Dsl
5
+ class InvalidName < Exception; end
6
+
7
+ class BlankSlate < BasicObject
8
+ def self.__forbidden_names
9
+ Config.instance_methods + PERMANENT_METHODS
10
+ end
11
+
12
+ private
13
+
14
+ def __taken?(name)
15
+ Config.instance_methods.include? name
16
+ end
17
+
18
+ def __taken_error_message(name, usage)
19
+ "#{name} can't be used as #{usage}: it will collide with Coconut::Config methods"
20
+ end
21
+
22
+ def self.inherited(subclass)
23
+ __eraseable_methods.each{ |method_name| undef_method method_name }
24
+ end
25
+
26
+ def self.__eraseable_methods
27
+ instance_methods - PERMANENT_METHODS
28
+ end
29
+
30
+ PERMANENT_METHODS = [:instance_eval, :__send__, :object_id, :__taken?, :__taken_error_message, :asset_folder]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'blank_slate'
2
+ require_relative '../config'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Environment < BlankSlate
7
+ def self.configure(&config)
8
+ new.configure(&config)
9
+ end
10
+
11
+ def initialize
12
+ @properties = {}
13
+ end
14
+
15
+ def configure(&config)
16
+ instance_eval &config
17
+ @properties
18
+ end
19
+
20
+ private
21
+
22
+ def method_missing(name, *args, &block)
23
+ ::Kernel::raise InvalidName, __taken_error_message(name, 'property name') if __taken?(name)
24
+ @properties[name] = args.first
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Coconut
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -2,7 +2,6 @@ require 'coconut'
2
2
 
3
3
  class MyClass; end
4
4
  module MyModule; end
5
- module MyConfig; def self.config; end; end;
6
5
 
7
6
  describe Coconut do
8
7
  it 'defines a config method in the provided namespace' do
@@ -12,9 +11,28 @@ describe Coconut do
12
11
  MyModule.should respond_to(:config)
13
12
  end
14
13
 
15
- it 'raises an error if the namespace already has a config method' do
16
- expect { Coconut.configure(MyConfig, :asset){} }.to raise_error
14
+ it "allows the app's configuration to be run twice" do
15
+ Coconut.configure(MyClass) { asset { env(:development){ property 'initial value' } } }
16
+ Coconut.configure(MyClass) { asset { env(:development){ property 'latest value' } } }
17
+ MyClass::config.asset.property.should eq 'latest value'
17
18
  end
18
- end
19
19
 
20
+ context 'finding out the environment' do
21
+ before { Coconut.instance_variable_set(:@__coconut_environment, nil) }
22
+
23
+ it 'uses the expression provided by the user if any' do
24
+ Coconut.take_environment_from { :somewhere }
25
+ Coconut.environment.should eq :somewhere
26
+ end
27
+
28
+ it 'uses the RACK_ENV environment variable by default' do
29
+ ENV['RACK_ENV'] = 'rack_env'
30
+ Coconut.environment.should eq 'rack_env'
31
+ end
20
32
 
33
+ it 'uses :development as environment if RACK_ENV is not defined' do
34
+ ENV['RACK_ENV'] = nil
35
+ Coconut.environment.should eq :development
36
+ end
37
+ end
38
+ end
@@ -1,34 +1,34 @@
1
1
  require 'coconut/config'
2
2
 
3
3
  describe Coconut::Config do
4
- let(:asset_config) { Coconut::Asset }
4
+ subject { described_class.new(application) }
5
+ let(:application) { Hash[twitter: properties] }
6
+ let(:properties) { Hash[property: 'value', other: 'other'] }
5
7
 
6
- it 'configures a series of assets' do
7
- subject = described_class.new(:current) do
8
- ftp {}
9
- ssh {}
8
+ context 'newly created' do
9
+ it 'has no property methods' do
10
+ (subject.methods - Object.instance_methods).should eq [:to_hash]
10
11
  end
11
- subject.should have_key 'ftp'
12
- subject.should have_key 'ssh'
13
- end
14
12
 
15
- it 'runs the configuration of each asset' do
16
- ftp_config = lambda {}
17
- ssh_config = lambda {}
18
- asset_config.should_receive(:new).with(:current, &ftp_config)
19
- asset_config.should_receive(:new).with(:current, &ssh_config)
20
- described_class.new(:current) do
21
- ftp &ftp_config
22
- ssh &ssh_config
13
+ it 'responds to every property' do
14
+ subject.should respond_to :twitter
23
15
  end
24
16
  end
25
17
 
26
- it 'assets can have (almost) any name' do
27
- subject = described_class.new(:current) do
28
- __id__ {}
29
- equal? {}
30
- inspect {}
18
+ context 'when a property is queried' do
19
+ it 'returns the property value' do
20
+ subject.twitter.property.should eq 'value'
21
+ subject.twitter.other.should eq 'other'
22
+ end
23
+
24
+ it 'defines a new method to access the property' do
25
+ subject.methods.should_not include(:twitter)
26
+ subject.twitter
27
+ subject.methods.should include(:twitter)
31
28
  end
32
- ['__id__', 'equal?', 'inspect'].each { |key| subject.should have_key(key) }
29
+ end
30
+
31
+ it 'can be transformed to a hash' do
32
+ subject.to_hash.should eq application
33
33
  end
34
34
  end
@@ -0,0 +1,24 @@
1
+ require 'coconut/dsl/application'
2
+
3
+ describe Coconut::Dsl::Application do
4
+ it 'creates the config for every asset' do
5
+ config = described_class.new(:current).run do
6
+ asset { environment(:current) { property 'value' } }
7
+ end
8
+ config.asset.property.should eq 'value'
9
+ end
10
+
11
+ it "doesn't allow assets with colliding names" do
12
+ expect {
13
+ described_class.configure(:current) { to_s {} }
14
+ }.to raise_error Coconut::Dsl::InvalidName, /to_s/
15
+ end
16
+
17
+ it 'can load the asset configuration from a folder' do
18
+ path = '.'
19
+ Coconut::Dsl::AssetFolder.stub(:config_from).with(path, ['config.rb']).
20
+ and_return("asset { environment(:current) { property 'value' } }")
21
+ config = described_class.configure(:current) { asset_folder path }
22
+ config.asset.property.should eq 'value'
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ require 'coconut/dsl/asset'
2
+
3
+ describe Coconut::Dsl::Asset do
4
+ it 'creates the asset config for the given environment' do
5
+ asset_config = described_class.configure(:current) do
6
+ environment(:other) { property 'value in other' }
7
+ environment(:current) { property 'value in current' }
8
+ end
9
+ asset_config.fetch(:property).should eq 'value in current'
10
+ end
11
+
12
+ it 'can setup different environments at the same time' do
13
+ asset_config = described_class.configure(:current) do
14
+ environment(:other, :current) { property 'value in other and current' }
15
+ end
16
+ asset_config.fetch(:property).should eq 'value in other and current'
17
+ end
18
+
19
+ it 'can setup environments step by step' do
20
+ asset_config = described_class.configure(:current) do
21
+ environment(:other, :current) { property 'value in other and current' }
22
+ environment(:other) { thing 'thing in other' }
23
+ environment(:current) { thing 'thing in current' }
24
+ end
25
+ asset_config.fetch(:property).should eq 'value in other and current'
26
+ asset_config.fetch(:thing).should eq 'thing in current'
27
+ end
28
+
29
+ it 'allows properties to be overriden' do
30
+ asset_config = described_class.configure(:current) do
31
+ environment(:other, :current) { property 'value in other and current' }
32
+ environment(:current) { property 'only in current' }
33
+ end
34
+ asset_config.fetch(:property).should eq 'only in current'
35
+ end
36
+
37
+ it 'has alternate ways of configuring an environment' do
38
+ asset_config = described_class.configure(:current) do
39
+ env(:current) { property1 1 }
40
+ environment(:current) { property2 2 }
41
+ environments(:current) { property3 3 }
42
+ envs(:current) { property4 4 }
43
+ end
44
+ asset_config.fetch(:property1).should eq 1
45
+ asset_config.fetch(:property2).should eq 2
46
+ asset_config.fetch(:property3).should eq 3
47
+ asset_config.fetch(:property4).should eq 4
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ require 'coconut/dsl/asset_folder'
2
+
3
+ describe 'Assets configuration from files in folder', integration: true do
4
+ let(:path) { '/tmp/coconut-testing/' }
5
+ let(:ignored) { ['config.rb'] }
6
+
7
+ let(:config) { "" }
8
+ let(:s3_config) { "s3 { environment(:current) { property 'p1' } }" }
9
+ let(:db_config) { "database { environment(:current) { property 'p2' } }" }
10
+
11
+ before :all do
12
+ `rm -rf #{path}`
13
+ Dir::mkdir path
14
+ File.open(File.join(path, 'config.rb'), 'w+') { |f| f.write '' }
15
+ File.open(File.join(path, 's3.rb'), 'w+') { |f| f.write s3_config }
16
+ File.open(File.join(path, 'db.rb'), 'w+') { |f| f.write db_config }
17
+ end
18
+
19
+ it 'evals every asset file in the folder as application config' do
20
+ configuration_from_folder.should eq "#{db_config}\n#{s3_config}"
21
+ end
22
+
23
+ context 'with other files that are not Ruby files' do
24
+ before do
25
+ File.open(File.join(path, 'readme.md'), 'w+') { |f| f.write 'readme' }
26
+ end
27
+
28
+ it 'ignores non Ruby files' do
29
+ configuration_from_folder.should eq "#{db_config}\n#{s3_config}"
30
+ end
31
+ end
32
+
33
+ def configuration_from_folder
34
+ Coconut::Dsl::AssetFolder.config_from(path, ignored)
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ require 'coconut/dsl/blank_slate'
2
+
3
+ describe Coconut::Dsl::BlankSlate do
4
+ it 'knows the forbidden names' do
5
+ expected_names = described_class::PERMANENT_METHODS + [:one]
6
+ Coconut::Config.stub(:instance_methods).and_return([:one])
7
+ described_class.__forbidden_names.should include(*expected_names)
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ require 'coconut/dsl/environment'
2
+
3
+ describe Coconut::Dsl::Environment do
4
+ it 'creates an environment config' do
5
+ config = described_class.configure do
6
+ property 'value'
7
+ other 'other'
8
+ end
9
+ config.fetch(:property).should eq 'value'
10
+ config.fetch(:other).should eq 'other'
11
+ end
12
+
13
+ it "doesn't allow properties with colliding names" do
14
+ expect {
15
+ described_class.configure { object_id '11' }
16
+ }.to raise_error Coconut::Dsl::InvalidName, /object_id/
17
+ end
18
+
19
+ it 'can take procs as property values' do
20
+ config = described_class.configure do
21
+ property1 Proc.new { 'value' }
22
+ property2 -> { 'value' }
23
+ property3 Kernel::lambda { 'value' }
24
+ end
25
+ config.fetch(:property1).should be_a_kind_of Proc
26
+ config.fetch(:property2).should be_a_kind_of Proc
27
+ config.fetch(:property3).should be_a_kind_of Proc
28
+ end
29
+
30
+ it 'can take expressions as property values' do
31
+ config = described_class.configure do
32
+ property1 2 + 2
33
+ property2 'aeiou'[0..2]
34
+ property3 [:a, :e, :i, :o, :u].take(3)
35
+ end
36
+ config.fetch(:property1).should eq 4
37
+ config.fetch(:property2).should eq 'aei'
38
+ config.fetch(:property3).should eq [:a, :e, :i]
39
+ end
40
+
41
+ it 'can take constants and scoped method calls as property values' do
42
+ config = described_class.configure do
43
+ arguments Math::PI
44
+ encodings Encoding.list
45
+ end
46
+ config.fetch(:arguments).should eq Math::PI
47
+ config.fetch(:encodings).should eq Encoding.list
48
+ end
49
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coconut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,17 +9,17 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-24 00:00:00.000000000 Z
12
+ date: 2012-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: hashie
15
+ name: cucumber
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
21
  version: '0'
22
- type: :runtime
22
+ type: :development
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
@@ -28,7 +28,7 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
30
  - !ruby/object:Gem::Dependency
31
- name: cucumber
31
+ name: rake
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
@@ -44,7 +44,7 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: rake
47
+ name: rspec
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: rspec
63
+ name: yard
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
@@ -88,20 +88,27 @@ files:
88
88
  - LICENSE.txt
89
89
  - README.md
90
90
  - Rakefile
91
+ - changelog.md
91
92
  - coconut.gemspec
93
+ - features/folder_configuration.feature
92
94
  - features/single_file_configuration.feature
93
95
  - features/step_definitions/configuration_steps.rb
94
96
  - features/support/env.rb
95
97
  - lib/coconut.rb
96
- - lib/coconut/asset.rb
97
98
  - lib/coconut/config.rb
98
- - lib/coconut/environment.rb
99
- - lib/coconut/runner.rb
99
+ - lib/coconut/dsl/application.rb
100
+ - lib/coconut/dsl/asset.rb
101
+ - lib/coconut/dsl/asset_folder.rb
102
+ - lib/coconut/dsl/blank_slate.rb
103
+ - lib/coconut/dsl/environment.rb
100
104
  - lib/coconut/version.rb
101
- - spec/coconut/asset_spec.rb
102
105
  - spec/coconut/coconut_spec.rb
103
106
  - spec/coconut/config_spec.rb
104
- - spec/coconut/environment_spec.rb
107
+ - spec/coconut/dsl/application_spec.rb
108
+ - spec/coconut/dsl/asset_spec.rb
109
+ - spec/coconut/dsl/assets_folder_spec.rb
110
+ - spec/coconut/dsl/blank_slate_spec.rb
111
+ - spec/coconut/dsl/environment_spec.rb
105
112
  homepage: http://github.com/jacegu/coconut
106
113
  licenses: []
107
114
  post_install_message:
@@ -116,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
123
  version: '0'
117
124
  segments:
118
125
  - 0
119
- hash: 3927338585615885072
126
+ hash: -443559654264598590
120
127
  required_rubygems_version: !ruby/object:Gem::Requirement
121
128
  none: false
122
129
  requirements:
@@ -125,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
132
  version: '0'
126
133
  segments:
127
134
  - 0
128
- hash: 3927338585615885072
135
+ hash: -443559654264598590
129
136
  requirements: []
130
137
  rubyforge_project:
131
138
  rubygems_version: 1.8.24
@@ -134,10 +141,15 @@ specification_version: 3
134
141
  summary: Coconut is a simple DSL that allows you to easily write and query your application's
135
142
  configuration with pure Ruby.
136
143
  test_files:
144
+ - features/folder_configuration.feature
137
145
  - features/single_file_configuration.feature
138
146
  - features/step_definitions/configuration_steps.rb
139
147
  - features/support/env.rb
140
- - spec/coconut/asset_spec.rb
141
148
  - spec/coconut/coconut_spec.rb
142
149
  - spec/coconut/config_spec.rb
143
- - spec/coconut/environment_spec.rb
150
+ - spec/coconut/dsl/application_spec.rb
151
+ - spec/coconut/dsl/asset_spec.rb
152
+ - spec/coconut/dsl/assets_folder_spec.rb
153
+ - spec/coconut/dsl/blank_slate_spec.rb
154
+ - spec/coconut/dsl/environment_spec.rb
155
+ has_rdoc:
@@ -1,27 +0,0 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'environment'
4
-
5
- module Coconut
6
- class Asset < DelegateClass(Hashie::Mash)
7
- def initialize(current_environment, &config)
8
- super Hashie::Mash.new
9
- run config, current_environment if block_given?
10
- end
11
-
12
- def environment(environment_name, &config)
13
- merge! Environment.new(&config) if current? environment_name
14
- end
15
-
16
- private
17
-
18
- def run(config, environment)
19
- @current_environment = environment
20
- instance_eval &config
21
- end
22
-
23
- def current?(environment)
24
- environment.to_sym == @current_environment.to_sym
25
- end
26
- end
27
- end
@@ -1,22 +0,0 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'runner'
4
-
5
- module Coconut
6
- class Environment < DelegateClass(Hashie::Mash)
7
- def initialize(&config)
8
- super Hashie::Mash.new
9
- run config
10
- end
11
-
12
- private
13
-
14
- def run(config)
15
- Runner.new run: config, method_missing: method(:configure_property)
16
- end
17
-
18
- def configure_property(name, values)
19
- self[name] = values.first
20
- end
21
- end
22
- end
@@ -1,28 +0,0 @@
1
- module Coconut
2
- class BlankSlate < BasicObject
3
- private
4
-
5
- def self.inherited(subclass)
6
- eraseable_methods.each{ |method_name| undef_method method_name }
7
- end
8
-
9
- def self.eraseable_methods
10
- instance_methods - PERMANENT_METHODS
11
- end
12
-
13
- PERMANENT_METHODS = [:instance_eval, :__send__, :object_id]
14
- end
15
-
16
- class Runner < BlankSlate
17
- def initialize(options)
18
- @callback = options[:method_missing]
19
- instance_eval &options[:run]
20
- end
21
-
22
- private
23
-
24
- def method_missing(name, *args, &block)
25
- @callback.(name, args, &block)
26
- end
27
- end
28
- end
@@ -1,12 +0,0 @@
1
- require 'coconut/asset'
2
-
3
- describe Coconut::Asset do
4
- it "knows the asset's config on the current environment" do
5
- subject = described_class.new(:current) do
6
- environment(:other) { property 'value on other' }
7
- environment(:current) { property 'value on current' }
8
- end
9
- subject.should have_key 'property'
10
- subject.fetch('property').should eq 'value on current'
11
- end
12
- end
@@ -1,21 +0,0 @@
1
- require 'hashie'
2
- require 'coconut/environment'
3
-
4
- describe Coconut::Environment do
5
- it 'collects method calls as configuration values' do
6
- config = described_class.new { property 'value' }
7
- config.should have_key :property
8
- config.property.should eq 'value'
9
- end
10
-
11
- it 'can have (almost) any property name' do
12
- config = described_class.new do
13
- __id__ 11
14
- equal? 33
15
- inspect 'CoconutJuice'
16
- end
17
- config.fetch('__id__').should eq 11
18
- config.fetch('equal?').should eq 33
19
- config.fetch('inspect').should eq 'CoconutJuice'
20
- end
21
- end