flip_the_switch 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.travis.yml +3 -1
  4. data/CHANGELOG.md +10 -0
  5. data/Classes/FlipTheSwitch/Shared/FTSFeature.h +11 -0
  6. data/Classes/FlipTheSwitch/Shared/FTSFeature.m +67 -0
  7. data/Classes/FlipTheSwitch/Shared/FTSFlipTheSwitch.h +21 -0
  8. data/Classes/FlipTheSwitch/Shared/FTSFlipTheSwitch.m +144 -0
  9. data/Classes/FlipTheSwitch/iOS/FTSFeatureCell.h +17 -0
  10. data/Classes/FlipTheSwitch/iOS/FTSFeatureCell.m +12 -0
  11. data/Classes/FlipTheSwitch/iOS/FTSFeatureConfigurationViewController.h +5 -0
  12. data/Classes/FlipTheSwitch/iOS/FTSFeatureConfigurationViewController.m +142 -0
  13. data/Classes/FlipTheSwitch/iOS/FTSFeatureDescriptionLabel.h +5 -0
  14. data/Classes/FlipTheSwitch/iOS/FTSFeatureDescriptionLabel.m +13 -0
  15. data/Example/Colors-OSX/AppDelegate.h +7 -0
  16. data/Example/Colors-OSX/AppDelegate.m +17 -0
  17. data/Example/Colors-OSX/Base.lproj/Main.storyboard +744 -0
  18. data/Example/Colors-OSX/ColoredViews/GreenView.h +5 -0
  19. data/Example/Colors-OSX/ColoredViews/GreenView.m +12 -0
  20. data/Example/Colors-OSX/ColoredViews/PurpleView.h +5 -0
  21. data/Example/Colors-OSX/ColoredViews/PurpleView.m +12 -0
  22. data/Example/Colors-OSX/ColoredViews/RedView.h +5 -0
  23. data/Example/Colors-OSX/ColoredViews/RedView.m +12 -0
  24. data/Example/Colors-OSX/ColoredViews/YellowView.h +5 -0
  25. data/Example/Colors-OSX/ColoredViews/YellowView.m +12 -0
  26. data/Example/Colors-OSX/FTSFlipTheSwitch+Features.h +45 -0
  27. data/Example/Colors-OSX/FTSFlipTheSwitch+Features.m +165 -0
  28. data/Example/Colors-OSX/Features.plist +39 -0
  29. data/Example/Colors-OSX/Images.xcassets/AppIcon.appiconset/Contents.json +58 -0
  30. data/Example/Colors-OSX/Info.plist +32 -0
  31. data/Example/Colors-OSX/Settings.bundle/Features.plist +59 -0
  32. data/Example/Colors-OSX/Settings.bundle/Root.plist +23 -0
  33. data/Example/Colors-OSX/ViewController.h +14 -0
  34. data/Example/Colors-OSX/ViewController.m +152 -0
  35. data/Example/Colors-OSX/main.m +5 -0
  36. data/Example/Colors-iOS/Base.lproj/Main.storyboard +111 -0
  37. data/Example/Colors-iOS/Colors-iOS-Info.plist +2 -2
  38. data/Example/Colors-iOS/FTSFlipTheSwitch+Features.h +45 -0
  39. data/Example/Colors-iOS/FTSFlipTheSwitch+Features.m +165 -0
  40. data/Example/Colors-iOS/Images.xcassets/AppIcon.appiconset/Contents.json +15 -0
  41. data/Example/Colors-iOS/ViewControlleriOS.m +55 -20
  42. data/Example/Colors.xcodeproj/project.pbxproj +273 -25
  43. data/Example/Colors.xcodeproj/xcshareddata/xcschemes/Colors-OSX.xcscheme +115 -0
  44. data/Example/Colors.xcodeproj/xcshareddata/xcschemes/Colors-iOS.xcscheme +9 -15
  45. data/Example/FTSFlipTheSwitch+Features.h +45 -0
  46. data/Example/FTSFlipTheSwitch+Features.m +165 -0
  47. data/Example/Features.plist +39 -0
  48. data/Example/Gemfile +4 -0
  49. data/Example/Podfile +11 -2
  50. data/Example/README.md +13 -0
  51. data/Example/Rakefile +5 -1
  52. data/Example/features.json +23 -0
  53. data/FlipTheSwitch.podspec +9 -4
  54. data/Gemfile +2 -1
  55. data/README.md +101 -21
  56. data/Resources/FlipTheSwitch/FlipTheSwitch.storyboard +208 -0
  57. data/Tests/FlipTheSwitchSpec-iOS/Features.plist +6 -1
  58. data/Tests/{FlipTheSwitchSpec-Mac/Features.plist → FlipTheSwitchSpec-iOS/OtherFeatures.plist} +5 -2
  59. data/Tests/FlipTheSwitchSpec.xcodeproj/project.pbxproj +101 -38
  60. data/Tests/FlipTheSwitchSpec.xcodeproj/xcshareddata/xcschemes/FlipTheSwitchSpec-Mac.xcscheme +9 -7
  61. data/Tests/FlipTheSwitchSpec.xcodeproj/xcshareddata/xcschemes/FlipTheSwitchSpec-iOS.xcscheme +9 -7
  62. data/Tests/Rakefile +1 -1
  63. data/Tests/Spec/Classes/FlipTheSwitch/FlipTheSwitchSpec.m +134 -28
  64. data/flip_the_switch.gemspec +2 -1
  65. data/images/feature_configuration_screen.png +0 -0
  66. data/lib/flip_the_switch.rb +2 -0
  67. data/lib/flip_the_switch/cli.rb +16 -24
  68. data/lib/flip_the_switch/environment.rb +11 -0
  69. data/lib/flip_the_switch/feature.rb +11 -0
  70. data/lib/flip_the_switch/generator/base.rb +3 -3
  71. data/lib/flip_the_switch/generator/category.rb +13 -5
  72. data/lib/flip_the_switch/generator/header.h.erb +15 -8
  73. data/lib/flip_the_switch/generator/implementation.m.erb +36 -12
  74. data/lib/flip_the_switch/generator/plist.rb +19 -1
  75. data/lib/flip_the_switch/generator/settings.rb +6 -6
  76. data/lib/flip_the_switch/reader/features.rb +136 -22
  77. data/spec/flip_the_switch/cli_spec.rb +12 -12
  78. data/spec/flip_the_switch/environment_spec.rb +20 -0
  79. data/spec/flip_the_switch/feature_spec.rb +20 -0
  80. data/spec/flip_the_switch/generator/category_spec.rb +14 -8
  81. data/spec/flip_the_switch/generator/plist_spec.rb +23 -7
  82. data/spec/flip_the_switch/generator/settings_spec.rb +6 -3
  83. data/spec/flip_the_switch/reader/defaults_spec.rb +0 -18
  84. data/spec/flip_the_switch/reader/features_spec.rb +74 -17
  85. data/spec/resources/ExpectedFeatures.plist +10 -2
  86. data/spec/resources/expected_header.h +8 -3
  87. data/spec/resources/expected_implementation.m +37 -10
  88. data/spec/resources/invalid_layout/features.json +7 -0
  89. data/spec/resources/invalid_type/features.json +23 -0
  90. data/spec/resources/real/features.json +38 -0
  91. data/tmp/.keep +0 -0
  92. metadata +74 -16
  93. data/Classes/FlipTheSwitch/FlipTheSwitch.h +0 -9
  94. data/Classes/FlipTheSwitch/FlipTheSwitch.m +0 -88
  95. data/Example/Colors-iOS/Base.lproj/Main_iPhone.storyboard +0 -76
  96. data/Example/features.yml +0 -3
  97. data/Tests/Spec/Helpers/GcovTestObserver.h +0 -4
  98. data/Tests/Spec/Helpers/GcovTestObserver.m +0 -12
  99. data/spec/resources/invalid_layout/features.yml +0 -4
  100. data/spec/resources/invalid_type/features.yml +0 -7
  101. data/spec/resources/real/features.yml +0 -6
@@ -4,12 +4,13 @@ Gem::Specification.new do |s|
4
4
  s.email = %w(mg.england@gmail.com rob@robsiwek.com)
5
5
  s.summary = 'A simple library to help enabling/disabling features on iOS/Mac applications.'
6
6
  s.homepage = 'https://github.com/michaelengland/FlipTheSwitch'
7
- s.version = '0.4.0'
7
+ s.version = '1.0.0'
8
8
  s.license = 'MIT'
9
9
 
10
10
  s.add_dependency 'activesupport', '~> 3.2'
11
11
  s.add_dependency 'thor', '~> 0.19'
12
12
  s.add_dependency 'plist', '~> 3.1'
13
+ s.add_dependency 'json-schema', '~> 2.5'
13
14
 
14
15
  s.files = `git ls-files`.split($\)
15
16
  s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
@@ -1,3 +1,5 @@
1
1
  require 'flip_the_switch/errors'
2
+ require 'flip_the_switch/feature'
3
+ require 'flip_the_switch/environment'
2
4
  require 'flip_the_switch/reader'
3
5
  require 'flip_the_switch/generator'
@@ -9,25 +9,26 @@ module FlipTheSwitch
9
9
  end
10
10
 
11
11
  public
12
- class_option :input, type: :string, aliases: '-i', default: defaults[:input], desc: 'Location of the directory containing features.yml file to read'
12
+ class_option :input, type: :string, aliases: '-i', default: defaults[:input], desc: 'Filename or directory containing features.yml file to read'
13
13
  class_option :environment, type: :string, aliases: '-n', default: defaults[:environment], desc: 'Name of environment to read from in features.yml file'
14
- class_option :enabled, type: :string, aliases: '-e', default: defaults[:enabled], desc: 'Extra features to be set as enabled'
15
- class_option :disabled, type: :string, aliases: '-d', default: defaults[:disabled], desc: 'Extra features to be set as disabled'
16
14
 
17
15
  desc 'plist', 'Auto-generates a Features.plist file for enabled/disabled features'
18
- method_option :output, type: :string, aliases: '-o', default: defaults[:plist_output], desc: 'Location of the directory in which Features.plist file will be created'
16
+ method_option :output, type: :string, aliases: '-o', default: defaults[:plist_output], desc: 'Filename or directory in which Features.plist file will be created'
17
+
19
18
  def plist
20
19
  plist_generator.generate
21
20
  end
22
21
 
23
22
  desc 'category', 'Auto-generates .h & .m files for enabled/disabled features'
24
23
  method_option :output, type: :string, aliases: '-o', default: defaults[:category_output], desc: 'Location of the directory in which FlipTheSwitch+Features.{h,m} files will be created'
24
+
25
25
  def category
26
26
  category_generator.generate
27
27
  end
28
28
 
29
29
  desc 'settings', 'Auto-generates settings.bundle files for enabling/disabling features from iOS settings menu'
30
30
  method_option :output, type: :string, aliases: '-o', default: defaults[:settings_output], desc: 'Location of the directory in which FlipTheSwitch+Features.{h,m} files will be created'
31
+
31
32
  def settings
32
33
  settings_generator.generate
33
34
  end
@@ -35,43 +36,34 @@ module FlipTheSwitch
35
36
  private
36
37
 
37
38
  def plist_generator
38
- Generator::Plist.new(output, feature_states)
39
+ Generator::Plist.new(output, features)
39
40
  end
40
41
 
41
42
  def category_generator
42
- Generator::Category.new(output, feature_states)
43
+ Generator::Category.new(output, features)
43
44
  end
44
45
 
45
46
  def settings_generator
46
- Generator::Settings.new(output, feature_states)
47
+ Generator::Settings.new(output, features)
47
48
  end
48
49
 
49
50
  def output
50
51
  options['output']
51
52
  end
52
53
 
53
- def feature_states
54
- feature_reader.feature_states.
55
- merge(enabled_states).
56
- merge(disabled_states)
54
+ def features
55
+ feature_reader.features
57
56
  end
58
57
 
59
58
  def feature_reader
60
- Reader::Features.new(options['input'], options['environment'])
61
- end
62
-
63
- def enabled_states
64
- states_for(options['enabled'].split(','), true)
65
- end
66
-
67
- def disabled_states
68
- states_for(options['disabled'].split(','), false)
59
+ Reader::Features.new(input, options['environment'])
69
60
  end
70
61
 
71
- def states_for(array, default)
72
- array.inject({}) do |h, feature|
73
- h[feature] = default
74
- h
62
+ def input
63
+ if File.directory?(options['input'])
64
+ File.join(options['input'], 'features.json')
65
+ else
66
+ options['input']
75
67
  end
76
68
  end
77
69
  end
@@ -0,0 +1,11 @@
1
+ module FlipTheSwitch
2
+ class Environment < Struct.new(:name, :features, :parent_name)
3
+ def initialize(name, features = [], parent_name = nil)
4
+ super(name, features, parent_name)
5
+ end
6
+
7
+ def has_parent?
8
+ parent_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module FlipTheSwitch
2
+ class Feature < Struct.new(:name, :enabled, :description, :sub_features, :parent_name)
3
+ def initialize(name, enabled, description = nil, sub_features = [], parent_name = nil)
4
+ super(name, enabled, description, sub_features, parent_name)
5
+ end
6
+
7
+ def has_parent?
8
+ parent_name
9
+ end
10
+ end
11
+ end
@@ -1,13 +1,13 @@
1
1
  module FlipTheSwitch
2
2
  module Generator
3
3
  class Base
4
- def initialize(output, feature_states)
4
+ def initialize(output, features)
5
5
  @output = output
6
- @feature_states = feature_states
6
+ @features = features
7
7
  end
8
8
 
9
9
  protected
10
- attr_reader :output, :feature_states
10
+ attr_reader :output, :features
11
11
  end
12
12
  end
13
13
  end
@@ -33,11 +33,11 @@ module FlipTheSwitch
33
33
  end
34
34
 
35
35
  def header_file
36
- File.join(output, 'FlipTheSwitch+Features.h')
36
+ File.join(output, 'FTSFlipTheSwitch+Features.h')
37
37
  end
38
38
 
39
39
  def implementation_file
40
- File.join(output, 'FlipTheSwitch+Features.m')
40
+ File.join(output, 'FTSFlipTheSwitch+Features.m')
41
41
  end
42
42
 
43
43
  def header
@@ -49,7 +49,7 @@ module FlipTheSwitch
49
49
  end
50
50
 
51
51
  def render(template)
52
- ERB.new(template).result(binding)
52
+ ERB.new(template, nil, '-').result(binding)
53
53
  end
54
54
 
55
55
  def header_template
@@ -64,8 +64,16 @@ module FlipTheSwitch
64
64
  File.read(File.expand_path("../#{name}.erb", __FILE__))
65
65
  end
66
66
 
67
- def feature_names
68
- feature_states.keys.map(&:to_s)
67
+ def all_features
68
+ features.flat_map { |feature|
69
+ feature_and_sub_features(feature)
70
+ }
71
+ end
72
+
73
+ def feature_and_sub_features(feature)
74
+ [feature] + feature.sub_features.flat_map { |sub_feature|
75
+ feature_and_sub_features(sub_feature)
76
+ }
69
77
  end
70
78
  end
71
79
  end
@@ -1,11 +1,18 @@
1
1
  /* AUTO-GENERATED. DO NOT ALTER */
2
- #import <FlipTheSwitch/FlipTheSwitch.h>
2
+ #import <FlipTheSwitch/FTSFlipTheSwitch.h>
3
3
 
4
- @interface FlipTheSwitch (Features)
5
- <% for feature in feature_names %>
6
- + (BOOL)is<%= feature.camelize %>Enabled;
7
- + (void)enable<%= feature.camelize %>;
8
- + (void)disable<%= feature.camelize %>;
9
- + (void)set<%= feature.camelize %>Enabled:(BOOL)enabled;
10
- <% end %>
4
+ @interface FTSFlipTheSwitch (Features)
5
+ <% all_features.each do |feature| -%>
6
+ <% if feature.description -%>
7
+ // <%= feature.description %>
8
+ <% end -%>
9
+ + (BOOL)is<%= feature.name.camelize %>Enabled;
10
+ + (void)enable<%= feature.name.camelize %>;
11
+ + (void)disable<%= feature.name.camelize %>;
12
+ + (void)set<%= feature.name.camelize %>Enabled:(BOOL)enabled;
13
+ + (void)reset<%= feature.name.camelize %>;
14
+ + (NSString *)<%= feature.name.camelize(:lower) %>Key;
15
+
16
+ <% end -%>
17
+ + (void)resetAll;
11
18
  @end
@@ -1,26 +1,50 @@
1
1
  /* AUTO-GENERATED. DO NOT ALTER */
2
- #import "FlipTheSwitch+Features.h"
2
+ #import "FTSFlipTheSwitch+Features.h"
3
3
 
4
- @implementation FlipTheSwitch (Features)
5
- <% for feature in feature_names %>
6
- + (BOOL)is<%= feature.camelize %>Enabled
4
+ @implementation FTSFlipTheSwitch (Features)
5
+
6
+ <% all_features.each do |feature| -%>
7
+ + (BOOL)is<%= feature.name.camelize %>Enabled
8
+ {
9
+ <% if feature.has_parent? -%>
10
+ return [self is<%= feature.parent_name.camelize %>Enabled] &&
11
+ [[FTSFlipTheSwitch sharedInstance] isFeatureEnabled:[self <%= feature.name.camelize(:lower) %>Key]];
12
+ <% else -%>
13
+ return [[FTSFlipTheSwitch sharedInstance] isFeatureEnabled:[self <%= feature.name.camelize(:lower) %>Key]];
14
+ <% end -%>
15
+ }
16
+
17
+ + (void)enable<%= feature.name.camelize %>
7
18
  {
8
- return [[FlipTheSwitch sharedInstance] isFeatureEnabled:@"<%= feature %>"];
19
+ [[FTSFlipTheSwitch sharedInstance] enableFeature:[self <%= feature.name.camelize(:lower) %>Key]];
9
20
  }
10
21
 
11
- + (void)enable<%= feature.camelize %>
22
+ + (void)disable<%= feature.name.camelize %>
12
23
  {
13
- [[FlipTheSwitch sharedInstance] enableFeature:@"<%= feature %>"];
24
+ [[FTSFlipTheSwitch sharedInstance] disableFeature:[self <%= feature.name.camelize(:lower) %>Key]];
14
25
  }
15
26
 
16
- + (void)disable<%= feature.camelize %>
27
+ + (void)set<%= feature.name.camelize %>Enabled:(BOOL)enabled
17
28
  {
18
- [[FlipTheSwitch sharedInstance] disableFeature:@"<%= feature %>"];
29
+ [[FTSFlipTheSwitch sharedInstance] setFeature:[self <%= feature.name.camelize(:lower) %>Key] enabled:enabled];
19
30
  }
20
31
 
21
- + (void)set<%= feature.camelize %>Enabled:(BOOL)enabled
32
+ + (void)reset<%= feature.name.camelize %>
22
33
  {
23
- [[FlipTheSwitch sharedInstance] setFeature:@"<%= feature %>" enabled:enabled];
34
+ [[FTSFlipTheSwitch sharedInstance] resetFeature:[self <%= feature.name.camelize(:lower) %>Key]];
24
35
  }
25
- <% end %>
36
+
37
+ + (NSString *)<%= feature.name.camelize(:lower) %>Key
38
+ {
39
+ return @"<%= feature.name %>";
40
+ }
41
+
42
+ <% end -%>
43
+ + (void)resetAll
44
+ {
45
+ <% all_features.each do |feature| -%>
46
+ [self reset<%= feature.name.camelize %>];
47
+ <% end -%>
48
+ }
49
+
26
50
  @end
@@ -10,8 +10,26 @@ module FlipTheSwitch
10
10
 
11
11
  private
12
12
 
13
+ def feature_states
14
+ features.inject({}) do |states, feature|
15
+ states.merge(feature.name => feature_hash(feature))
16
+ end
17
+ end
18
+
19
+ def feature_hash(feature)
20
+ if feature.description
21
+ {enabled: feature.enabled, description: feature.description}
22
+ else
23
+ {enabled: feature.enabled}
24
+ end
25
+ end
26
+
13
27
  def output_file
14
- File.join(output, 'Features.plist')
28
+ if File.directory?(output)
29
+ File.join(output, 'Features.plist')
30
+ else
31
+ output
32
+ end
15
33
  end
16
34
  end
17
35
  end
@@ -47,7 +47,7 @@ module FlipTheSwitch
47
47
  end
48
48
 
49
49
  def write_features_plist
50
- ::Plist::Emit.save_plist(features, features_plist)
50
+ ::Plist::Emit.save_plist(feature_properties, features_plist)
51
51
  end
52
52
 
53
53
  def delete_file(file)
@@ -82,17 +82,17 @@ module FlipTheSwitch
82
82
  ]
83
83
  end
84
84
 
85
- def features
85
+ def feature_properties
86
86
  {PreferenceSpecifiers: feature_toggles}
87
87
  end
88
88
 
89
89
  def feature_toggles
90
- feature_states.map { |feature, state|
90
+ features.map { |feature|
91
91
  {
92
92
  Type: 'PSToggleSwitchSpecifier',
93
- Title: feature,
94
- Key: "FTS_FEATURE_#{feature}",
95
- DefaultValue: state
93
+ Title: feature.name,
94
+ Key: "FTS_FEATURE_#{feature.name}",
95
+ DefaultValue: feature.enabled
96
96
  }
97
97
  }
98
98
  end
@@ -1,4 +1,5 @@
1
- require 'yaml'
1
+ require 'json'
2
+ require 'json-schema'
2
3
 
3
4
  module FlipTheSwitch
4
5
  module Reader
@@ -8,45 +9,158 @@ module FlipTheSwitch
8
9
  @environment = environment
9
10
  end
10
11
 
11
- def feature_states
12
- if valid_file?
13
- environment_states
14
- else
15
- raise Error::InvalidFile.new(input_file)
16
- end
12
+ def features
13
+ raise Error::InvalidFile.new(input_file) unless valid_file?
14
+ raise Error::InvalidEnvironment.new(environment) unless environments_by_name.has_key?(environment)
15
+ inherited_environment(environment).features
17
16
  end
18
17
 
19
18
  private
20
19
  attr_reader :input, :environment
21
20
 
22
- def valid_file?
23
- file_states.is_a?(Hash) && file_states.all? { |environment, feature|
24
- environment.is_a?(String) && valid_feature_hash?(feature)
25
- }
21
+ INHERITS_KEY = 'inherits_from'
22
+ ENABLED_KEY = 'enabled'
23
+ DESCRIPTION_KEY = 'description'
24
+
25
+ def inherited_environment(env_name)
26
+ inherited_env = environments_by_name[env_name]
27
+ if inherited_env.has_parent?
28
+ merge_environments(inherited_env, inherited_environment(inherited_env.parent_name))
29
+ else
30
+ inherited_env
31
+ end
32
+ end
33
+
34
+ def merge_environments(overriding_env, parent_env)
35
+ Environment.new(
36
+ overriding_env.name,
37
+ merge_features(parent_env.features, overriding_env.features),
38
+ overriding_env.parent_name
39
+ )
26
40
  end
27
41
 
28
- def valid_feature_hash?(feature_hash)
29
- feature_hash.is_a?(Hash) && feature_hash.all? { |feature, state|
30
- feature.is_a?(String) && !!state == state
42
+ def merge_features(parent_features, overriding_features)
43
+ parent_features.inject([]) { |merged_features, parent_feature|
44
+ overriding_feature = overriding_features.detect { |feature| feature.name == parent_feature.name }
45
+ if overriding_feature
46
+ merged_features.push(merge_feature(parent_feature, overriding_feature))
47
+ else
48
+ merged_features.push(parent_feature)
49
+ end
31
50
  }
32
51
  end
33
52
 
34
- def environment_states
35
- if file_states.has_key?(environment)
36
- file_states[environment]
53
+ def merge_feature(parent_feature, child_feature)
54
+ Feature.new(parent_feature.name,
55
+ (child_feature.enabled != nil) ? child_feature.enabled : parent_feature.enabled,
56
+ child_feature.description ? child_feature.description : parent_feature.description,
57
+ sub_features(child_feature, parent_feature),
58
+ child_feature.parent_name
59
+ )
60
+ end
61
+
62
+ def sub_features(child_feature, parent_feature)
63
+ if !child_feature.sub_features.empty?
64
+ merge_sub_features(parent_feature.sub_features, child_feature.sub_features)
37
65
  else
38
- raise Error::InvalidEnvironment.new(environment)
66
+ parent_feature.sub_features
39
67
  end
40
68
  end
41
69
 
42
- def file_states
43
- @file_states ||= YAML.load_file(input_file)
70
+ def merge_sub_features(parent_feature_sub_features, child_feature_sub_features)
71
+ child_feature_sub_features.inject([]) { |merged_sub_features, child_sub_feature|
72
+ merged_sub_features.push(merge_feature(related_parents_sub_feature(parent_feature_sub_features, child_sub_feature.name), child_sub_feature))
73
+ }
74
+ end
75
+
76
+ def related_parents_sub_feature(parent_feature_sub_features, sub_feature_name)
77
+ parent_feature_sub_features.detect { |parent_sub_feature|
78
+ parent_sub_feature.name == sub_feature_name
79
+ }
80
+ end
81
+
82
+ def environments_by_name
83
+ parse.inject({}) { |hash, env|
84
+ hash[env.name] = env
85
+ hash
86
+ }
87
+ end
88
+
89
+ def parse
90
+ @parse ||= json.map { |env_name, env_info|
91
+ parse_environment(env_name, env_info)
92
+ }
93
+ end
94
+
95
+ def parse_environment(name, info)
96
+ Environment.new(name, parse_environment_features(info), info[INHERITS_KEY])
97
+ end
98
+
99
+ def parse_environment_features(info)
100
+ info.select { |key, _|
101
+ key != INHERITS_KEY
102
+ }.map { |feature_name, feature_info|
103
+ parse_feature(feature_name, feature_info, nil)
104
+ }
105
+ end
106
+
107
+ def parse_feature(name, info, parent_name)
108
+ Feature.new(name, info.fetch(ENABLED_KEY), info[DESCRIPTION_KEY], parse_sub_features(info, name), parent_name)
109
+ end
110
+
111
+ def parse_sub_features(info, parent_name)
112
+ info.select { |key, _| ![ENABLED_KEY, DESCRIPTION_KEY].include?(key) }.map { |sub_name, sub_info|
113
+ parse_feature(sub_name, sub_info, parent_name)
114
+ }
115
+ end
116
+
117
+ def json
118
+ JSON.parse(input_file)
119
+ end
120
+
121
+ def input_file
122
+ File.read(input)
44
123
  rescue SystemCallError => e
45
124
  raise Error::UnreadableFile.new(e)
46
125
  end
47
126
 
48
- def input_file
49
- File.join(input, 'features.yml')
127
+ def valid_file?
128
+ JSON::Validator.validate(expected_schema, input_file)
129
+ end
130
+
131
+ def expected_schema
132
+ {
133
+ type: :object,
134
+ additionalProperties: {
135
+ type: :object,
136
+ additionalProperties: {
137
+ properties: {
138
+ enabled: {
139
+ type: :boolean
140
+ },
141
+ description: {
142
+ type: :string
143
+ },
144
+ additionalProperties: {
145
+ properties: {
146
+ enabled: {
147
+ type: :boolean
148
+ },
149
+ description: {
150
+ type: :string
151
+ },
152
+ required: [
153
+ :enabled
154
+ ]
155
+ }
156
+ }
157
+ },
158
+ required: [
159
+ :enabled
160
+ ]
161
+ }
162
+ }
163
+ }
50
164
  end
51
165
  end
52
166
  end