├── CODEOWNERS ├── spec ├── fixtures │ ├── test_utils │ │ └── resources │ │ │ ├── bom.conf │ │ │ ├── subdir │ │ │ ├── bar.conf │ │ │ ├── baz.conf │ │ │ └── foo.conf │ │ │ ├── cycle.conf │ │ │ ├── utf8.conf │ │ │ ├── ᚠᛇᚻ.conf │ │ │ ├── test01.json │ │ │ ├── utf16.conf │ │ │ ├── file-include.conf │ │ │ ├── include-from-list.conf │ │ │ ├── test03.conf │ │ │ └── test01.conf │ ├── parse_render │ │ ├── example3 │ │ │ ├── output.conf │ │ │ └── input.conf │ │ ├── example4 │ │ │ ├── output.conf │ │ │ └── input.json │ │ ├── example2 │ │ │ ├── input.conf │ │ │ ├── output.conf │ │ │ └── output_nocomments.conf │ │ └── example1 │ │ │ ├── output_nocomments.conf │ │ │ ├── input.conf │ │ │ └── output.conf │ └── hocon │ │ ├── by_extension │ │ ├── cat.test-json │ │ ├── cat.conf │ │ └── cat.test │ │ └── with_substitution │ │ └── subst.conf ├── unit │ ├── typesafe │ │ └── config │ │ │ ├── README.md │ │ │ ├── config_value_factory_spec.rb │ │ │ ├── config_factory_spec.rb │ │ │ ├── simple_config_spec.rb │ │ │ ├── token_spec.rb │ │ │ └── path_spec.rb │ ├── hocon │ │ ├── README.md │ │ └── hocon_spec.rb │ └── cli │ │ └── cli_spec.rb └── spec_helper.rb ├── lib ├── hocon │ ├── version.rb │ ├── impl.rb │ ├── parser.rb │ ├── impl │ │ ├── full_includer.rb │ │ ├── unsupported_operation_error.rb │ │ ├── config_include_kind.rb │ │ ├── mergeable_value.rb │ │ ├── config_node_array.rb │ │ ├── config_node_concatenation.rb │ │ ├── array_iterator.rb │ │ ├── config_node_single_token.rb │ │ ├── resolve_memos.rb │ │ ├── resolve_status.rb │ │ ├── abstract_config_node_value.rb │ │ ├── origin_type.rb │ │ ├── from_map_mode.rb │ │ ├── unmergeable.rb │ │ ├── config_null.rb │ │ ├── config_boolean.rb │ │ ├── config_node_comment.rb │ │ ├── abstract_config_node.rb │ │ ├── replaceable_merge_stack.rb │ │ ├── substitution_expression.rb │ │ ├── config_node_include.rb │ │ ├── config_int.rb │ │ ├── config_double.rb │ │ ├── resolve_result.rb │ │ ├── simple_include_context.rb │ │ ├── url.rb │ │ ├── memo_key.rb │ │ ├── token.rb │ │ ├── container.rb │ │ ├── path_builder.rb │ │ ├── token_type.rb │ │ ├── config_node_path.rb │ │ ├── config_node_simple_value.rb │ │ ├── simple_config_document.rb │ │ ├── config_node_complex_value.rb │ │ ├── config_number.rb │ │ ├── config_string.rb │ │ ├── config_node_field.rb │ │ ├── config_node_root.rb │ │ ├── config_impl_util.rb │ │ ├── default_transformer.rb │ │ ├── config_reference.rb │ │ ├── path.rb │ │ ├── abstract_config_object.rb │ │ ├── simple_includer.rb │ │ ├── resolve_context.rb │ │ └── path_parser.rb │ ├── config_syntax.rb │ ├── config_resolve_options.rb │ ├── config_value_type.rb │ ├── config_includer_file.rb │ ├── parser │ │ ├── config_node.rb │ │ ├── config_document_factory.rb │ │ └── config_document.rb │ ├── config_render_options.rb │ ├── config_parseable.rb │ ├── config_list.rb │ ├── config_error.rb │ ├── config_include_context.rb │ ├── config_parse_options.rb │ ├── config_util.rb │ ├── config_factory.rb │ ├── config_mergeable.rb │ ├── config_value_factory.rb │ ├── config_value.rb │ ├── config_object.rb │ └── cli.rb └── hocon.rb ├── bin └── hocon ├── Gemfile ├── .gitignore ├── .github └── workflows │ ├── rspec_tests.yaml │ └── mend.yaml ├── hocon.gemspec ├── CHANGELOG.md └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/phoenix 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/bom.conf: -------------------------------------------------------------------------------- 1 | # 2 | foo = bar 3 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/subdir/bar.conf: -------------------------------------------------------------------------------- 1 | bar=43 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/subdir/baz.conf: -------------------------------------------------------------------------------- 1 | baz=45 2 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example3/output.conf: -------------------------------------------------------------------------------- 1 | a=true 2 | b=true -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example3/input.conf: -------------------------------------------------------------------------------- 1 | a: true 2 | b: ${a} 3 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/cycle.conf: -------------------------------------------------------------------------------- 1 | include "cycle.conf" 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/utf8.conf: -------------------------------------------------------------------------------- 1 | # 2 | ᚠᛇᚻ = ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢ 3 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/ᚠᛇᚻ.conf: -------------------------------------------------------------------------------- 1 | # 2 | ᚠᛇᚻ = ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢ 3 | -------------------------------------------------------------------------------- /spec/fixtures/hocon/by_extension/cat.test-json: -------------------------------------------------------------------------------- 1 | { 2 | "meow": "cats" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/hocon/by_extension/cat.conf: -------------------------------------------------------------------------------- 1 | # Comment 2 | { 3 | "meow": "cats" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/hocon/by_extension/cat.test: -------------------------------------------------------------------------------- 1 | # Comment 2 | { 3 | "meow": "cats" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/hocon/with_substitution/subst.conf: -------------------------------------------------------------------------------- 1 | a: true 2 | b: ${a} 3 | c: ${ENVARRAY} 4 | -------------------------------------------------------------------------------- /lib/hocon/version.rb: -------------------------------------------------------------------------------- 1 | module Hocon 2 | module Version 3 | STRING = '1.4.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/test01.json: -------------------------------------------------------------------------------- 1 | { 2 | "fromJson1" : 1, 3 | "fromJsonA" : "A" 4 | } -------------------------------------------------------------------------------- /lib/hocon/impl.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | module Hocon::Impl 6 | 7 | end -------------------------------------------------------------------------------- /bin/hocon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'hocon/cli' 3 | 4 | Hocon::CLI.main(Hocon::CLI.parse_args(ARGV)) 5 | 6 | -------------------------------------------------------------------------------- /lib/hocon/parser.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | module Hocon::Parser 6 | 7 | end -------------------------------------------------------------------------------- /lib/hocon/impl/full_includer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | class Hocon::Impl::FullIncluder 6 | end -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/utf16.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/ruby-hocon/HEAD/spec/fixtures/test_utils/resources/utf16.conf -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example4/output.conf: -------------------------------------------------------------------------------- 1 | { 2 | "kermit"="frog", 3 | "miss"="piggy", 4 | "bert"="ernie", 5 | "janice"="guitar" 6 | } -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example4/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "kermit": "frog", 3 | "miss": "piggy", 4 | "bert": "ernie", 5 | "janice": "guitar" 6 | } -------------------------------------------------------------------------------- /spec/unit/typesafe/config/README.md: -------------------------------------------------------------------------------- 1 | ## TESTS PORTED FROM UPSTREAM 2 | 3 | This directory should only contain tests that are ported from the upstream 4 | Java library. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in hocon.gemspec 4 | gemspec 5 | 6 | group :tests do 7 | gem 'rspec', '~> 3.0' 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/subdir/foo.conf: -------------------------------------------------------------------------------- 1 | foo=42 2 | # included without file() 3 | include "bar.conf" 4 | # included using file() 5 | include file("bar-file.conf") 6 | -------------------------------------------------------------------------------- /lib/hocon/impl/unsupported_operation_error.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | class Hocon::Impl::UnsupportedOperationError < StandardError 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/file-include.conf: -------------------------------------------------------------------------------- 1 | base=41 2 | # included without file() in a subdir 3 | include "subdir/foo.conf" 4 | # included using file() in a subdir 5 | include file("subdir/baz.conf") 6 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/include-from-list.conf: -------------------------------------------------------------------------------- 1 | // The {} inside the [] is needed because 2 | // just [ include ] means an array with the 3 | // string "include" in it. 4 | a = [ { include "test01.conf" } ] 5 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_include_kind.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | module Hocon::Impl::ConfigIncludeKind 6 | URL = 0 7 | FILE = 1 8 | CLASSPATH = 2 9 | HEURISTIC = 3 10 | end 11 | -------------------------------------------------------------------------------- /lib/hocon/impl/mergeable_value.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_mergeable' 5 | 6 | class Hocon::Impl::MergeableValue < Hocon::ConfigMergeable 7 | # TODO 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | Gemfile.local 6 | Gemfile.lock 7 | coverage 8 | InstalledFiles 9 | lib/bundler/man 10 | pkg 11 | rdoc 12 | spec/reports 13 | test/tmp 14 | test/version_tmp 15 | tmp 16 | 17 | # YARD artifacts 18 | .yardoc 19 | _yardoc 20 | doc/ 21 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example2/input.conf: -------------------------------------------------------------------------------- 1 | jruby-puppet: { 2 | jruby-pools: [{environment: production}] 3 | load-path: [/usr/lib/ruby/site_ruby/1.8, /usr/lib/ruby/site_ruby/1.8] 4 | master-conf-dir: /etc/puppet 5 | master-var-dir: /var/lib/puppet 6 | } 7 | 8 | webserver: { 9 | host: 1.2.3.4 10 | } 11 | -------------------------------------------------------------------------------- /spec/unit/hocon/README.md: -------------------------------------------------------------------------------- 1 | ## RUBY-SPECIFIC TESTS 2 | 3 | This directory should only contain tests that are specific to the Ruby library/API. 4 | Tests ported from the upstream Java library should live in spec/typesafe/config. 5 | 6 | Where possible it would be good to avoid sharing fixtures between the two types 7 | of tests as well. -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_array.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_node_complex_value' 5 | 6 | class Hocon::Impl::ConfigNodeArray 7 | include Hocon::Impl::ConfigNodeComplexValue 8 | def new_node(nodes) 9 | self.class.new(nodes) 10 | end 11 | end -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_concatenation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_node_complex_value' 5 | 6 | class Hocon::Impl::ConfigNodeConcatenation 7 | include Hocon::Impl::ConfigNodeComplexValue 8 | def new_node(nodes) 9 | self.class.new(nodes) 10 | end 11 | end -------------------------------------------------------------------------------- /lib/hocon/config_syntax.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | module Hocon::ConfigSyntax 6 | JSON = 0 7 | CONF = 1 8 | # alias 'HOCON' to 'CONF' since some users may be more familiar with that 9 | HOCON = 1 10 | # we're not going to try to support .properties files any time soon :) 11 | #PROPERTIES = 2 12 | end 13 | -------------------------------------------------------------------------------- /lib/hocon/impl/array_iterator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | class Hocon::Impl::ArrayIterator 6 | def initialize(a) 7 | @a = a 8 | @index = 0 9 | end 10 | 11 | def has_next? 12 | @index < @a.length 13 | end 14 | 15 | def next 16 | @index += 1 17 | @a[@index - 1] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example1/output_nocomments.conf: -------------------------------------------------------------------------------- 1 | foo { 2 | bar { 3 | falsy=false 4 | truthy=true 5 | yahoo=yippee 6 | baz=42 7 | boom=[ 8 | 1, 9 | 2, 10 | { 11 | derp=duh 12 | }, 13 | 4 14 | ] 15 | abracadabra=hi 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_single_token.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_node' 5 | 6 | class Hocon::Impl::ConfigNodeSingleToken 7 | include Hocon::Impl::AbstractConfigNode 8 | def initialize(t) 9 | @token = t 10 | end 11 | 12 | attr_reader :token 13 | 14 | def tokens 15 | [@token] 16 | end 17 | end -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example2/output.conf: -------------------------------------------------------------------------------- 1 | jruby-puppet { 2 | jruby-pools=[ 3 | { 4 | environment=production 5 | } 6 | ] 7 | load-path=[ 8 | "/usr/lib/ruby/site_ruby/1.8", 9 | "/usr/lib/ruby/site_ruby/1.8" 10 | ] 11 | master-conf-dir="/etc/puppet" 12 | master-var-dir="/var/lib/puppet" 13 | } 14 | 15 | webserver { 16 | host="1.2.3.4" 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example2/output_nocomments.conf: -------------------------------------------------------------------------------- 1 | jruby-puppet { 2 | jruby-pools=[ 3 | { 4 | environment=production 5 | } 6 | ] 7 | load-path=[ 8 | "/usr/lib/ruby/site_ruby/1.8", 9 | "/usr/lib/ruby/site_ruby/1.8" 10 | ] 11 | master-conf-dir="/etc/puppet" 12 | master-var-dir="/var/lib/puppet" 13 | } 14 | 15 | webserver { 16 | host="1.2.3.4" 17 | } 18 | -------------------------------------------------------------------------------- /lib/hocon/impl/resolve_memos.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon' 4 | require_relative '../../hocon/impl' 5 | 6 | class Hocon::Impl::ResolveMemos 7 | 8 | def initialize(memos = {}) 9 | @memos = memos 10 | end 11 | 12 | def get(key) 13 | @memos[key] 14 | end 15 | 16 | def put(key, value) 17 | copy = @memos.clone 18 | copy[key] = value 19 | Hocon::Impl::ResolveMemos.new(copy) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/hocon/impl/resolve_status.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | class Hocon::Impl::ResolveStatus 6 | UNRESOLVED = 0 7 | RESOLVED = 1 8 | 9 | def self.from_values(values) 10 | if values.any? { |v| v.resolve_status == UNRESOLVED } 11 | UNRESOLVED 12 | else 13 | RESOLVED 14 | end 15 | end 16 | 17 | def self.from_boolean(resolved) 18 | resolved ? RESOLVED : UNRESOLVED 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example1/input.conf: -------------------------------------------------------------------------------- 1 | # These are some opening comments 2 | # These are some additional opening comments 3 | foo.bar { 4 | // the baz is is blah blah 5 | baz = 42 6 | boom = [1, 2, {derp : duh }, 4] 7 | empty = [] 8 | 9 | # abracadabra setting 10 | abracadabra = "hi" 11 | } 12 | 13 | // as for the yippee 14 | # it entails some things 15 | foo.bar.yahoo = "yippee" 16 | 17 | # truthy 18 | foo.bar.truthy = true 19 | 20 | # falsy 21 | foo.bar.falsy = false -------------------------------------------------------------------------------- /lib/hocon/impl/abstract_config_node_value.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_node' 5 | 6 | # This essentially exists in the upstream so we can ensure only certain types of 7 | # config nodes can be passed into some methods. That's not a problem in Ruby, so this is 8 | # unnecessary, but it seems best to keep it around for consistency 9 | module Hocon::Impl::AbstractConfigNodeValue 10 | include Hocon::Impl::AbstractConfigNode 11 | end -------------------------------------------------------------------------------- /lib/hocon/impl/origin_type.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | module Hocon::Impl::OriginType 6 | ## for now, we only support a subset of these 7 | GENERIC = 0 8 | FILE = 1 9 | #URL = 2 10 | # We don't actually support loading from the classpath / loadpath, which is 11 | # what 'RESOURCE' is about in the upstream library. However, some code paths 12 | # still flow through our simplistic implementation of `ParseableResource`, so 13 | # we need this constant. 14 | RESOURCE = 3 15 | end 16 | -------------------------------------------------------------------------------- /lib/hocon/impl/from_map_mode.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_error' 5 | 6 | module Hocon::Impl::FromMapMode 7 | KEYS_ARE_PATHS = 0 8 | KEYS_ARE_KEYS = 1 9 | ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 10 | 11 | def self.map_mode_name(from_map_mode) 12 | case from_map_mode 13 | when KEYS_ARE_PATHS then "KEYS_ARE_PATHS" 14 | when KEYS_ARE_KEYS then "KEYS_ARE_KEYS" 15 | else raise ConfigBugOrBrokenError.new("Unrecognized FromMapMode #{from_map_mode}") 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/hocon/impl/unmergeable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_error' 5 | 6 | 7 | # 8 | # Interface that tags a ConfigValue that is not mergeable until after 9 | # substitutions are resolved. Basically these are special ConfigValue that 10 | # never appear in a resolved tree, like {@link ConfigSubstitution} and 11 | # {@link ConfigDelayedMerge}. 12 | # 13 | module Hocon::Impl::Unmergeable 14 | def unmerged_values 15 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Unmergeable` must implement `unmerged_values` (#{self.class})" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/fixtures/parse_render/example1/output.conf: -------------------------------------------------------------------------------- 1 | foo={ 2 | # These are some opening comments 3 | # These are some additional opening comments 4 | bar={ 5 | # falsy 6 | falsy=false 7 | # truthy 8 | truthy=true 9 | # as for the yippee 10 | # it entails some things 11 | yahoo=yippee 12 | # the baz is is blah blah 13 | baz=42 14 | boom=[ 15 | 1, 16 | 2, 17 | { 18 | derp=duh 19 | }, 20 | 4 21 | ] 22 | empty=[] 23 | # abracadabra setting 24 | abracadabra=hi 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_null.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_value_type' 5 | 6 | class Hocon::Impl::ConfigNull 7 | include Hocon::Impl::AbstractConfigValue 8 | 9 | def initialize(origin) 10 | super(origin) 11 | end 12 | 13 | def value_type 14 | Hocon::ConfigValueType::NULL 15 | end 16 | 17 | def unwrapped 18 | nil 19 | end 20 | 21 | def transform_to_string 22 | "null" 23 | end 24 | 25 | def render_value_to_sb(sb, indent, at_root, options) 26 | sb << "null" 27 | end 28 | 29 | def new_copy(origin) 30 | self.class.new(origin) 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_boolean.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_value' 5 | 6 | class Hocon::Impl::ConfigBoolean 7 | include Hocon::Impl::AbstractConfigValue 8 | 9 | def initialize(origin, value) 10 | super(origin) 11 | @value = value 12 | end 13 | 14 | attr_reader :value 15 | 16 | def value_type 17 | Hocon::ConfigValueType::BOOLEAN 18 | end 19 | 20 | def unwrapped 21 | @value 22 | end 23 | 24 | def transform_to_string 25 | @value.to_s 26 | end 27 | 28 | def new_copy(origin) 29 | Hocon::Impl::ConfigBoolean.new(origin, @value) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_comment.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_error' 5 | require_relative '../../hocon/impl/config_node_single_token' 6 | require_relative '../../hocon/impl/tokens' 7 | 8 | class Hocon::Impl::ConfigNodeComment < Hocon::Impl::ConfigNodeSingleToken 9 | def initialize(comment) 10 | super(comment) 11 | unless Hocon::Impl::Tokens.comment?(@token) 12 | raise Hocon::ConfigError::ConfigBugOrBrokenError, 'Tried to create a ConfigNodeComment from a non-comment token' 13 | end 14 | end 15 | 16 | def comment_text 17 | Hocon::Impl::Tokens.comment_text(@token) 18 | end 19 | end -------------------------------------------------------------------------------- /lib/hocon/config_resolve_options.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | class Hocon::ConfigResolveOptions 6 | attr_reader :use_system_environment, :allow_unresolved 7 | 8 | def initialize(use_system_environment, allow_unresolved) 9 | @use_system_environment = use_system_environment 10 | @allow_unresolved = allow_unresolved 11 | end 12 | 13 | def set_use_system_environment(value) 14 | self.class.new(value, @allow_unresolved) 15 | end 16 | 17 | def set_allow_unresolved(value) 18 | self.class.new(@use_system_environment, value) 19 | end 20 | 21 | class << self 22 | 23 | def defaults 24 | self.new(true, false) 25 | end 26 | 27 | def no_system 28 | defaults.set_use_system_environment(false) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/hocon/impl/abstract_config_node.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/parser/config_node' 5 | require_relative '../../hocon/config_error' 6 | 7 | module Hocon::Impl::AbstractConfigNode 8 | include Hocon::Parser::ConfigNode 9 | def tokens 10 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of AbstractConfigNode should override `tokens` (#{self.class})" 11 | end 12 | 13 | def render 14 | orig_text = StringIO.new 15 | tokens.each do |t| 16 | orig_text << t.token_text 17 | end 18 | orig_text.string 19 | end 20 | 21 | def ==(other) 22 | other.is_a?(Hocon::Impl::AbstractConfigNode) && 23 | (render == other.render) 24 | end 25 | 26 | def hash 27 | render.hash 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hocon/config_value_type.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_error' 5 | 6 | # 7 | # The type of a configuration value (following the JSON type schema). 9 | # 10 | module Hocon::ConfigValueType 11 | OBJECT = 0 12 | LIST = 1 13 | NUMBER = 2 14 | BOOLEAN = 3 15 | NULL = 4 16 | STRING = 5 17 | 18 | def self.value_type_name(config_value_type) 19 | case config_value_type 20 | when OBJECT then "OBJECT" 21 | when LIST then "LIST" 22 | when NUMBER then "NUMBER" 23 | when BOOLEAN then "BOOLEAN" 24 | when NULL then "NULL" 25 | when STRING then "STRING" 26 | else raise Hocon::ConfigError::ConfigBugOrBrokenError, "Unrecognized value type '#{config_value_type}'" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hocon/impl/replaceable_merge_stack.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/container' 5 | require_relative '../../hocon/config_error' 6 | 7 | # 8 | # Implemented by a merge stack (ConfigDelayedMerge, ConfigDelayedMergeObject) 9 | # that replaces itself during substitution resolution in order to implement 10 | # "look backwards only" semantics. 11 | # 12 | module Hocon::Impl::ReplaceableMergeStack 13 | include Hocon::Impl::Container 14 | 15 | # 16 | # Make a replacement for this object skipping the given number of elements 17 | # which are lower in merge priority. 18 | # 19 | def make_replacement(context, skipping) 20 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ReplaceableMergeStack` must implement `make_replacement` (#{self.class})" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/hocon/impl/substitution_expression.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../hocon/impl' 2 | 3 | 4 | class Hocon::Impl::SubstitutionExpression 5 | 6 | def initialize(path, optional) 7 | @path = path 8 | @optional = optional 9 | end 10 | attr_reader :path, :optional 11 | 12 | def change_path(new_path) 13 | if new_path == @path 14 | self 15 | else 16 | Hocon::Impl::SubstitutionExpression.new(new_path, @optional) 17 | end 18 | end 19 | 20 | def to_s 21 | "${#{@optional ? "?" : ""}#{@path.render}}" 22 | end 23 | 24 | def ==(other) 25 | if other.is_a? Hocon::Impl::SubstitutionExpression 26 | other.path == @path && other.optional == @optional 27 | else 28 | false 29 | end 30 | end 31 | 32 | def hash 33 | h = 41 * (41 + @path.hash) 34 | h = 41 * (h + (optional ? 1 : 0)) 35 | 36 | h 37 | end 38 | end -------------------------------------------------------------------------------- /.github/workflows/rspec_tests.yaml: -------------------------------------------------------------------------------- 1 | name: RSpec tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | rspec_tests: 14 | name: RSpec (Ruby ${{ matrix.ruby }}) 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby: [ '2.7', '3.2' ] 19 | steps: 20 | - name: Checkout current PR 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Install Ruby version ${{ matrix.ruby }} 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | - name: Update rubygems and install gems 29 | run: | 30 | gem update --system --silent --no-document 31 | bundle install --jobs 4 --retry 3 32 | - run: bundle exec rspec spec 33 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_include.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_error' 5 | require_relative '../../hocon/impl/abstract_config_node' 6 | require_relative '../../hocon/impl/config_node_simple_value' 7 | 8 | class Hocon::Impl::ConfigNodeInclude 9 | include Hocon::Impl::AbstractConfigNode 10 | def initialize(children, kind) 11 | @children = children 12 | @kind = kind 13 | end 14 | 15 | attr_reader :kind, :children 16 | 17 | def tokens 18 | tokens = [] 19 | @children.each do |child| 20 | tokens += child.tokens 21 | end 22 | tokens 23 | end 24 | 25 | def name 26 | @children.each do |child| 27 | if child.is_a?(Hocon::Impl::ConfigNodeSimpleValue) 28 | return Hocon::Impl::Tokens.value(child.token).unwrapped 29 | end 30 | end 31 | nil 32 | end 33 | end -------------------------------------------------------------------------------- /lib/hocon/impl/config_int.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_number' 5 | require_relative '../../hocon/config_value_type' 6 | 7 | class Hocon::Impl::ConfigInt < Hocon::Impl::ConfigNumber 8 | def initialize(origin, value, original_text) 9 | super(origin, original_text) 10 | @value = value 11 | end 12 | 13 | attr_reader :value 14 | 15 | def value_type 16 | Hocon::ConfigValueType::NUMBER 17 | end 18 | 19 | def unwrapped 20 | @value 21 | end 22 | 23 | def transform_to_string 24 | s = super 25 | if s.nil? 26 | self.to_s 27 | else 28 | s 29 | end 30 | end 31 | 32 | def long_value 33 | @value 34 | end 35 | 36 | def double_value 37 | @value 38 | end 39 | 40 | def new_copy(origin) 41 | Hocon::Impl::ConfigInt.new(origin, @value, @original_text) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/test03.conf: -------------------------------------------------------------------------------- 1 | { 2 | "test01" : { 3 | "ints" : 12, 4 | include "test01", 5 | "booleans" : 42 6 | }, 7 | 8 | "test02" : { 9 | include 10 | 11 | "test02.conf" 12 | }, 13 | 14 | "equiv01" : { 15 | include "equiv01/original.json" 16 | }, 17 | 18 | # missing includes are supposed to be silently ignored 19 | nonexistent { 20 | include "nothere" 21 | include "nothere.conf" 22 | include "nothere.json" 23 | include "nothere.properties" 24 | } 25 | 26 | # make sure included file substitutions fall back to parent file, 27 | # both when the include is at the root (so doesn't need to have 28 | # substitutions adjusted) and when it is not. 29 | foo="This is in the including file" 30 | bar="This is in the including file" 31 | include "test03-included.conf" 32 | 33 | subtree { 34 | include "test03-included.conf" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hocon.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 2 | require 'hocon/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'hocon' 6 | s.version = Hocon::Version::STRING 7 | s.date = '2016-10-27' 8 | s.summary = "HOCON Config Library" 9 | s.description = "== A port of the Java {Typesafe Config}[https://github.com/typesafehub/config] library to Ruby" 10 | s.authors = ["Chris Price", "Wayne Warren", "Preben Ingvaldsen", "Joe Pinsonault", "Kevin Corcoran", "Jane Lu"] 11 | s.email = 'chris@puppetlabs.com' 12 | s.files = Dir["{lib}/**/*.rb", "bin/*", "LICENSE", "*.md"] 13 | s.require_paths = ["lib"] 14 | s.executables = ['hocon'] 15 | s.homepage = 'https://github.com/puppetlabs/ruby-hocon' 16 | s.license = 'Apache-2.0' 17 | s.required_ruby_version = '>=1.9.0' 18 | 19 | # Testing dependencies 20 | s.add_development_dependency 'rspec', '~> 2.14' 21 | end 22 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_double.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_number' 5 | 6 | class Hocon::Impl::ConfigDouble < Hocon::Impl::ConfigNumber 7 | def initialize(origin, value, original_text) 8 | super(origin, original_text) 9 | @value = value 10 | end 11 | 12 | attr_reader :value 13 | 14 | def value_type 15 | Hocon::ConfigValueType::NUMBER 16 | end 17 | 18 | def unwrapped 19 | @value 20 | end 21 | 22 | def transform_to_string 23 | s = super 24 | if s.nil? 25 | @value.to_s 26 | else 27 | s 28 | end 29 | end 30 | 31 | def long_value 32 | @value.to_i 33 | end 34 | 35 | def double_value 36 | @value 37 | end 38 | 39 | def new_copy(origin) 40 | self.class.new(origin, @value, original_text) 41 | end 42 | 43 | # NOTE: skipping `writeReplace` from upstream, because it involves serialization 44 | end 45 | -------------------------------------------------------------------------------- /lib/hocon/impl/resolve_result.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon' 4 | require_relative '../../hocon/impl' 5 | 6 | # value is allowed to be null 7 | class Hocon::Impl::ResolveResult 8 | ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 9 | 10 | attr_accessor :context, :value 11 | 12 | def initialize(context, value) 13 | @context = context 14 | @value = value 15 | end 16 | 17 | def self.make(context, value) 18 | self.new(context, value) 19 | end 20 | 21 | def as_object_result 22 | unless @value.is_a?(Hocon::Impl::AbstractConfigObject) 23 | raise ConfigBugOrBrokenError.new("Expecting a resolve result to be an object, but it was #{@value}") 24 | end 25 | self 26 | end 27 | 28 | def as_value_result 29 | self 30 | end 31 | 32 | def pop_trace 33 | self.class.make(@context.pop_trace, value) 34 | end 35 | 36 | def to_s 37 | "ResolveResult(#{@value})" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/hocon/impl/simple_include_context.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/simple_includer' 5 | require_relative '../../hocon/config_include_context' 6 | require_relative '../../hocon/impl/config_impl' 7 | 8 | class Hocon::Impl::SimpleIncludeContext 9 | include Hocon::ConfigIncludeContext 10 | 11 | def initialize(parseable) 12 | @parseable = parseable 13 | end 14 | 15 | def with_parseable(parseable) 16 | if parseable.equal?(@parseable) 17 | self 18 | else 19 | self.class.new(parseable) 20 | end 21 | end 22 | 23 | def relative_to(filename) 24 | if Hocon::Impl::ConfigImpl.trace_loads_enabled 25 | Hocon::Impl::ConfigImpl.trace("Looking for '#{filename}' relative to #{@parseable}") 26 | end 27 | if ! @parseable.nil? 28 | @parseable.relative_to(filename) 29 | else 30 | nil 31 | end 32 | end 33 | 34 | def parse_options 35 | Hocon::Impl::SimpleIncluder.clear_for_include(@parseable.options) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hocon/impl/url.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'uri' 4 | require_relative '../../hocon/impl' 5 | 6 | # There are several places in the Java codebase that 7 | # use Java's URL constructor, and rely on it to throw 8 | # a `MalformedURLException` if the URL isn't valid. 9 | # 10 | # Ruby doesn't really have a similar constructor / 11 | # validator, so this is a little shim to hopefully 12 | # make the ported code match up with the upstream more 13 | # closely. 14 | class Hocon::Impl::Url 15 | class MalformedUrlError < StandardError 16 | def initialize(msg, cause = nil) 17 | super(msg) 18 | @cause = cause 19 | end 20 | end 21 | 22 | def initialize(url) 23 | begin 24 | # URI::parse wants a string 25 | @url = URI.parse(url.to_s) 26 | if !(@url.kind_of?(URI::HTTP)) 27 | raise MalformedUrlError, "Unrecognized URL: '#{url}'" 28 | end 29 | rescue URI::InvalidURIError => e 30 | raise MalformedUrlError.new("Unrecognized URL: '#{url}' (error: #{e})", e) 31 | end 32 | end 33 | 34 | def to_s 35 | @url.to_s 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hocon/config_includer_file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_error' 5 | 6 | # 7 | # Implement this in addition to {@link ConfigIncluder} if you want to 8 | # support inclusion of files with the {@code include file("filename")} syntax. 9 | # If you do not implement this but do implement {@link ConfigIncluder}, 10 | # attempts to load files will use the default includer. 11 | # 12 | module Hocon::ConfigIncluderFile 13 | # 14 | # Parses another item to be included. The returned object typically would 15 | # not have substitutions resolved. You can throw a ConfigException here to 16 | # abort parsing, or return an empty object, but may not return null. 17 | # 18 | # @param context 19 | # some info about the include context 20 | # @param what 21 | # the include statement's argument 22 | # @return a non-null ConfigObject 23 | # 24 | def include_file(context, what) 25 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigIncluderFile` must implement `include_file` (#{self.class})" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hocon/impl/memo_key.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon' 4 | require_relative '../../hocon/impl' 5 | 6 | class Hocon::Impl::MemoKey 7 | 8 | def initialize(value, restrict_to_child_or_nil) 9 | @value = value 10 | @restrict_to_child_or_nil = restrict_to_child_or_nil 11 | end 12 | 13 | def hash 14 | h = @value.hash 15 | if @restrict_to_child_or_nil != nil 16 | h + 41 * (41 + @restrict_to_child_or_nil.hash) 17 | else 18 | h 19 | end 20 | end 21 | 22 | def ==(other) 23 | if other.is_a?(self.class) 24 | o = other 25 | if !o.value.equal?(@value) 26 | return false 27 | elsif o.restrict_to_child_or_nil.equals(@restrict_to_child_or_nil) 28 | return true 29 | elsif o.restrict_to_child_or_nil == nil || @restrict_to_child_or_nil == nil 30 | return false 31 | else 32 | return o.restrict_to_child_or_nil == @restrict_to_child_or_nil 33 | end 34 | else 35 | false 36 | end 37 | end 38 | 39 | def to_s 40 | "MemoKey(#{@value}@#{@value.hash},#{@restrict_to_child_or_nil})" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/hocon/impl/token.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/token_type' 5 | 6 | class Hocon::Impl::Token 7 | attr_reader :token_type, :token_text 8 | def self.new_without_origin(token_type, debug_string, token_text) 9 | Hocon::Impl::Token.new(token_type, nil, token_text, debug_string) 10 | end 11 | 12 | def initialize(token_type, origin, token_text = nil, debug_string = nil) 13 | @token_type = token_type 14 | @origin = origin 15 | @token_text = token_text 16 | @debug_string = debug_string 17 | end 18 | 19 | attr_reader :origin 20 | 21 | def line_number 22 | if @origin 23 | @origin.line_number 24 | else 25 | -1 26 | end 27 | end 28 | 29 | def to_s 30 | if !@debug_string.nil? 31 | @debug_string 32 | else 33 | Hocon::Impl::TokenType.token_type_name(@token_type) 34 | end 35 | end 36 | 37 | def ==(other) 38 | # @origin deliberately left out 39 | other.is_a?(Hocon::Impl::Token) && @token_type == other.token_type 40 | end 41 | 42 | def hash 43 | @token_type.hash 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.github/workflows/mend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mend Monitor 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | mend_monitor: 9 | if: ${{ github.repository_owner == 'puppetlabs' }} 10 | runs-on: ubuntu-latest 11 | name: Mend Monitor 12 | steps: 13 | - name: Checkout current PR 14 | uses: actions/checkout@v4 15 | - name: Setup Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | - name: Create lock 20 | run: bundle lock 21 | - uses: actions/setup-java@v3 22 | with: 23 | distribution: 'temurin' 24 | java-version: '17' 25 | - name: Download Mend 26 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 27 | - name: Run Mend 28 | run: java -jar wss-unified-agent.jar 29 | env: 30 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 31 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 32 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 33 | WS_PRODUCTNAME: Puppet Agent 34 | WS_PROJECTNAME: ${{ github.event.repository.name }} 35 | -------------------------------------------------------------------------------- /lib/hocon/parser/config_node.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/parser' 4 | require_relative '../../hocon/config_error' 5 | 6 | # 7 | # An immutable node that makes up the ConfigDocument AST, and which can be 8 | # used to reproduce part or all of the original text of an input. 9 | # 10 | #

11 | # Because this object is immutable, it is safe to use from multiple threads and 12 | # there's no need for "defensive copies." 13 | # 14 | #

15 | # Do not implement interface {@code ConfigNode}; it should only be 16 | # implemented by the config library. Arbitrary implementations will not work 17 | # because the library internals assume a specific concrete implementation. 18 | # Also, this interface is likely to grow new methods over time, so third-party 19 | # implementations will break. 20 | # 21 | 22 | module Hocon::Parser::ConfigNode 23 | # 24 | # The original text of the input which was used to form this particular node. 25 | # @return the original text used to form this node as a String 26 | # 27 | def render 28 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigNode should override `render` (#{self.class})" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/hocon/impl/container.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_value' 5 | require_relative '../../hocon/config_error' 6 | 7 | # An AbstractConfigValue which contains other values. Java has no way to 8 | # express "this has to be an AbstractConfigValue also" other than making 9 | # AbstractConfigValue an interface which would be aggravating. But we can say 10 | # we are a ConfigValue. 11 | module Hocon::Impl::Container 12 | include Hocon::ConfigValue 13 | # 14 | # Replace a child of this value. CAUTION if replacement is null, delete the 15 | # child, which may also delete the parent, or make the parent into a 16 | # non-container. 17 | # 18 | def replace_child(child, replacement) 19 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Container` must implement `replace_child` (#{self.class})" 20 | end 21 | 22 | # 23 | # Super-expensive full traversal to see if descendant is anywhere 24 | # underneath this container. 25 | # 26 | def has_descendant?(descendant) 27 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Container` must implement `has_descendant?` (#{self.class})" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hocon/parser/config_document_factory.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/parser' 4 | require_relative '../../hocon/impl/parseable' 5 | require_relative '../../hocon/config_parse_options' 6 | 7 | # 8 | # Factory for creating {@link 9 | # com.typesafe.config.parser.ConfigDocument} instances. 10 | # 11 | class Hocon::Parser::ConfigDocumentFactory 12 | # 13 | # Parses a file into a ConfigDocument instance. 14 | # 15 | # @param file 16 | # the file to parse 17 | # @param options 18 | # parse options to control how the file is interpreted 19 | # @return the parsed configuration 20 | # @throws com.typesafe.config.ConfigException on IO or parse errors 21 | # 22 | def self.parse_file(file, options = Hocon::ConfigParseOptions.defaults) 23 | Hocon::Impl::Parseable.new_file(file, options).parse_config_document 24 | end 25 | 26 | # 27 | # Parses a string which should be valid HOCON or JSON. 28 | # 29 | # @param s string to parse 30 | # @param options parse options 31 | # @return the parsed configuration 32 | # 33 | def self.parse_string(s, options = Hocon::ConfigParseOptions.defaults) 34 | Hocon::Impl::Parseable.new_string(s, options).parse_config_document 35 | end 36 | end -------------------------------------------------------------------------------- /lib/hocon/impl/path_builder.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/path' 5 | require_relative '../../hocon/config_error' 6 | 7 | class Hocon::Impl::PathBuilder 8 | 9 | def initialize 10 | @keys = [] 11 | @result = nil 12 | end 13 | 14 | def check_can_append 15 | if @result 16 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "Adding to PathBuilder after getting result" 17 | end 18 | end 19 | 20 | def append_key(key) 21 | check_can_append 22 | @keys.push(key) 23 | end 24 | 25 | def append_path(path) 26 | check_can_append 27 | 28 | first = path.first 29 | remainder = path.remainder 30 | 31 | loop do 32 | @keys.push(first) 33 | 34 | if !remainder.nil? 35 | first = remainder.first 36 | remainder = remainder.remainder 37 | else 38 | break 39 | end 40 | end 41 | end 42 | 43 | def result 44 | # note: if keys is empty, we want to return nil, which is a valid 45 | # empty path 46 | if @result.nil? 47 | remainder = nil 48 | while !@keys.empty? 49 | key = @keys.pop 50 | remainder = Hocon::Impl::Path.new(key, remainder) 51 | end 52 | @result = remainder 53 | end 54 | @result 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/hocon/impl/token_type.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | 5 | class Hocon::Impl::TokenType 6 | START = 0 7 | EOF = 1 8 | COMMA = 2 9 | EQUALS = 3 10 | COLON = 4 11 | OPEN_CURLY = 5 12 | CLOSE_CURLY = 6 13 | OPEN_SQUARE = 7 14 | CLOSE_SQUARE = 8 15 | VALUE = 9 16 | NEWLINE = 10 17 | UNQUOTED_TEXT = 11 18 | SUBSTITUTION = 12 19 | PROBLEM = 13 20 | COMMENT = 14 21 | PLUS_EQUALS = 15 22 | IGNORED_WHITESPACE = 16 23 | 24 | def self.token_type_name(token_type) 25 | case token_type 26 | when START then "START" 27 | when EOF then "EOF" 28 | when COMMA then "COMMA" 29 | when EQUALS then "EQUALS" 30 | when COLON then "COLON" 31 | when OPEN_CURLY then "OPEN_CURLY" 32 | when CLOSE_CURLY then "CLOSE_CURLY" 33 | when OPEN_SQUARE then "OPEN_SQUARE" 34 | when CLOSE_SQUARE then "CLOSE_SQUARE" 35 | when VALUE then "VALUE" 36 | when NEWLINE then "NEWLINE" 37 | when UNQUOTED_TEXT then "UNQUOTED_TEXT" 38 | when SUBSTITUTION then "SUBSTITUTION" 39 | when PROBLEM then "PROBLEM" 40 | when COMMENT then "COMMENT" 41 | when PLUS_EQUALS then "PLUS_EQUALS" 42 | when IGNORED_WHITESPACE then "IGNORED_WHITESPACE" 43 | else raise ConfigBugOrBrokenError, "Unrecognized token type #{token_type}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_path.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/tokens' 5 | require_relative '../../hocon/impl/abstract_config_node' 6 | 7 | class Hocon::Impl::ConfigNodePath 8 | include Hocon::Impl::AbstractConfigNode 9 | Tokens = Hocon::Impl::Tokens 10 | 11 | def initialize(path, tokens) 12 | @path = path 13 | @tokens = tokens 14 | end 15 | 16 | attr_reader :tokens 17 | 18 | def value 19 | @path 20 | end 21 | 22 | def sub_path(to_remove) 23 | period_count = 0 24 | tokens_copy = tokens.clone 25 | (0..tokens_copy.size - 1).each do |i| 26 | if Tokens.unquoted_text?(tokens_copy[i]) && 27 | tokens_copy[i].token_text == "." 28 | period_count += 1 29 | end 30 | 31 | if period_count == to_remove 32 | return self.class.new(@path.sub_path_to_end(to_remove), tokens_copy[i + 1..tokens_copy.size]) 33 | end 34 | end 35 | raise ConfigBugOrBrokenError, "Tried to remove too many elements from a Path node" 36 | end 37 | 38 | def first 39 | tokens_copy = tokens.clone 40 | (0..tokens_copy.size - 1).each do |i| 41 | if Tokens.unquoted_text?(tokens_copy[i]) && 42 | tokens_copy[i].token_text == "." 43 | return self.class.new(@path.sub_path(0, 1), tokens_copy[0, i]) 44 | end 45 | end 46 | self 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/hocon/config_render_options.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | class Hocon::ConfigRenderOptions 6 | def initialize(origin_comments, comments, formatted, json, key_value_separator=:equals) 7 | @origin_comments = origin_comments 8 | @comments = comments 9 | @formatted = formatted 10 | @json = json 11 | @key_value_separator = key_value_separator 12 | end 13 | 14 | attr_accessor :origin_comments, :comments, :formatted, :json, :key_value_separator 15 | 16 | def origin_comments? 17 | @origin_comments 18 | end 19 | def comments? 20 | @comments 21 | end 22 | def formatted? 23 | @formatted 24 | end 25 | def json? 26 | @json 27 | end 28 | 29 | # 30 | # Returns the default render options which are verbose (commented and 31 | # formatted). See {@link ConfigRenderOptions#concise} for stripped-down 32 | # options. This rendering will not be valid JSON since it has comments. 33 | # 34 | # @return the default render options 35 | # 36 | def self.defaults 37 | Hocon::ConfigRenderOptions.new(true, true, true, true) 38 | end 39 | 40 | # 41 | # Returns concise render options (no whitespace or comments). For a 42 | # resolved {@link Config}, the concise rendering will be valid JSON. 43 | # 44 | # @return the concise render options 45 | # 46 | def self.concise 47 | Hocon::ConfigRenderOptions.new(false, false, false, true) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") 4 | 5 | EXAMPLE1 = { :hash => 6 | {"foo" => { 7 | "bar" => { 8 | "baz" => 42, 9 | "abracadabra" => "hi", 10 | "yahoo" => "yippee", 11 | "boom" => [1, 2, {"derp" => "duh"}, 4], 12 | "empty" => [], 13 | "truthy" => true, 14 | "falsy" => false 15 | }}}, 16 | :name => "example1", 17 | } 18 | 19 | EXAMPLE2 = { :hash => 20 | {"jruby-puppet"=> { 21 | "jruby-pools" => [{"environment" => "production"}], 22 | "load-path" => ["/usr/lib/ruby/site_ruby/1.8", "/usr/lib/ruby/site_ruby/1.8"], 23 | "master-conf-dir" => "/etc/puppet", 24 | "master-var-dir" => "/var/lib/puppet", 25 | }, 26 | "webserver" => {"host" => "1.2.3.4"}}, 27 | :name => "example2", 28 | } 29 | 30 | EXAMPLE3 = { :hash => 31 | {"a" => true, 32 | "b" => true}, 33 | :name => "example3", 34 | } 35 | 36 | EXAMPLE4 = { :hash => 37 | {"kermit" => "frog", 38 | "miss" => "piggy", 39 | "bert" => "ernie", 40 | "janice" => "guitar"}, 41 | :name => "example4", 42 | } 43 | 44 | # set values out of order to verify they return in-order 45 | # must be set prior to config_impl.rb loading 46 | ENV['ENVARRAY.1'] = 'bar' 47 | ENV['ENVARRAY.2'] = 'baz' 48 | ENV['ENVARRAY.0'] = 'foo' 49 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_simple_value.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/config_error' 4 | require_relative '../../hocon/impl' 5 | require_relative '../../hocon/impl/abstract_config_node_value' 6 | require_relative '../../hocon/impl/array_iterator' 7 | require_relative '../../hocon/impl/config_reference' 8 | require_relative '../../hocon/impl/config_string' 9 | require_relative '../../hocon/impl/path_parser' 10 | require_relative '../../hocon/impl/substitution_expression' 11 | require_relative '../../hocon/impl/tokens' 12 | 13 | class Hocon::Impl::ConfigNodeSimpleValue 14 | include Hocon::Impl::AbstractConfigNodeValue 15 | 16 | Tokens = Hocon::Impl::Tokens 17 | 18 | def initialize(value) 19 | @token = value 20 | end 21 | 22 | attr_reader :token 23 | 24 | def tokens 25 | [@token] 26 | end 27 | 28 | def value 29 | if Tokens.value?(@token) 30 | return Tokens.value(@token) 31 | elsif Tokens.unquoted_text?(@token) 32 | return Hocon::Impl::ConfigString::Unquoted.new(@token.origin, Tokens.unquoted_text(@token)) 33 | elsif Tokens.substitution?(@token) 34 | expression = Tokens.get_substitution_path_expression(@token) 35 | path = Hocon::Impl::PathParser.parse_path_expression(Hocon::Impl::ArrayIterator.new(expression), @token.origin) 36 | optional = Tokens.get_substitution_optional(@token) 37 | 38 | return Hocon::Impl::ConfigReference.new(@token.origin, Hocon::Impl::SubstitutionExpression.new(path, optional)) 39 | end 40 | raise Hocon::ConfigError::ConfigBugOrBrokenError, 'ConfigNodeSimpleValue did not contain a valid value token' 41 | end 42 | end -------------------------------------------------------------------------------- /lib/hocon/impl/simple_config_document.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/parser/config_document' 5 | require_relative '../../hocon/impl/config_document_parser' 6 | require_relative '../../hocon/config_render_options' 7 | 8 | class Hocon::Impl::SimpleConfigDocument 9 | include Hocon::Parser::ConfigDocument 10 | 11 | def initialize(parsed_node, parse_options) 12 | @config_node_tree = parsed_node 13 | @parse_options = parse_options 14 | end 15 | 16 | def set_value(path, new_value) 17 | origin = Hocon::Impl::SimpleConfigOrigin.new_simple("single value parsing") 18 | reader = StringIO.new(new_value) 19 | tokens = Hocon::Impl::Tokenizer.tokenize(origin, reader, @parse_options.syntax) 20 | parsed_value = Hocon::Impl::ConfigDocumentParser.parse_value(tokens, origin, @parse_options) 21 | reader.close 22 | 23 | self.class.new(@config_node_tree.set_value(path, parsed_value, @parse_options.syntax), @parse_options) 24 | end 25 | 26 | def set_config_value(path, new_value) 27 | options = Hocon::ConfigRenderOptions.defaults 28 | options.origin_comments = false 29 | set_value(path, new_value.render(options).strip) 30 | end 31 | 32 | def remove_value(path) 33 | self.class.new(@config_node_tree.set_value(path, nil, @parse_options.syntax), @parse_options) 34 | end 35 | 36 | def has_value?(path) 37 | @config_node_tree.has_value(path) 38 | end 39 | 40 | def render 41 | @config_node_tree.render 42 | end 43 | 44 | def ==(other) 45 | other.class.ancestors.include?(Hocon::Parser::ConfigDocument) && render == other.render 46 | end 47 | 48 | def hash 49 | render.hash 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/hocon/config_parseable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_error' 5 | 6 | # 7 | # An opaque handle to something that can be parsed, obtained from 8 | # {@link ConfigIncludeContext}. 9 | # 10 | #

11 | # Do not implement this interface; it should only be implemented by 12 | # the config library. Arbitrary implementations will not work because the 13 | # library internals assume a specific concrete implementation. Also, this 14 | # interface is likely to grow new methods over time, so third-party 15 | # implementations will break. 16 | # 17 | module Hocon::ConfigParseable 18 | # 19 | # Parse whatever it is. The options should come from 20 | # {@link ConfigParseable#options options()} but you could tweak them if you 21 | # like. 22 | # 23 | # @param options 24 | # parse options, should be based on the ones from 25 | # {@link ConfigParseable#options options()} 26 | # @return the parsed object 27 | # 28 | def parse(options) 29 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigParseable` must implement `parse` (#{self.class})" 30 | end 31 | 32 | # 33 | # Returns a {@link ConfigOrigin} describing the origin of the parseable 34 | # item. 35 | # @return the origin of the parseable item 36 | # 37 | def origin 38 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigParseable` must implement `origin` (#{self.class})" 39 | end 40 | 41 | # 42 | # Get the initial options, which can be modified then passed to parse(). 43 | # These options will have the right description, includer, and other 44 | # parameters already set up. 45 | # @return the initial options 46 | # 47 | def options 48 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigParseable` must implement `options` (#{self.class})" 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/hocon/config_list.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_value' 5 | require_relative '../hocon/config_error' 6 | 7 | # 8 | # Subtype of {@link ConfigValue} representing a list value, as in JSON's 9 | # {@code [1,2,3]} syntax. 10 | # 11 | #

12 | # {@code ConfigList} implements {@code java.util.List} so you can 13 | # use it like a regular Java list. Or call {@link #unwrapped()} to unwrap the 14 | # list elements into plain Java values. 15 | # 16 | #

17 | # Like all {@link ConfigValue} subtypes, {@code ConfigList} is immutable. This 18 | # makes it threadsafe and you never have to create "defensive copies." The 19 | # mutator methods from {@link java.util.List} all throw 20 | # {@link java.lang.UnsupportedOperationException}. 21 | # 22 | #

23 | # The {@link ConfigValue#valueType} method on a list returns 24 | # {@link ConfigValueType#LIST}. 25 | # 26 | #

27 | # Do not implement {@code ConfigList}; it should only be implemented 28 | # by the config library. Arbitrary implementations will not work because the 29 | # library internals assume a specific concrete implementation. Also, this 30 | # interface is likely to grow new methods over time, so third-party 31 | # implementations will break. 32 | # 33 | # 34 | module Hocon::ConfigList 35 | include Hocon::ConfigValue 36 | 37 | # 38 | # Recursively unwraps the list, returning a list of plain Java values such 39 | # as Integer or String or whatever is in the list. 40 | # 41 | def unwrapped 42 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigValue should provide their own implementation of `unwrapped` (#{self.class})" 43 | end 44 | 45 | def with_origin(origin) 46 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigValue should provide their own implementation of `with_origin` (#{self.class})" 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_complex_value.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_node_value' 5 | require_relative '../../hocon/impl/config_node_field' 6 | require_relative '../../hocon/impl/config_node_include' 7 | require_relative '../../hocon/impl/config_node_single_token' 8 | require_relative '../../hocon/impl/tokens' 9 | require_relative '../../hocon/config_error' 10 | 11 | module Hocon::Impl::ConfigNodeComplexValue 12 | include Hocon::Impl::AbstractConfigNodeValue 13 | def initialize(children) 14 | @children = children 15 | end 16 | 17 | attr_reader :children 18 | 19 | def tokens 20 | tokens = [] 21 | @children.each do |child| 22 | tokens += child.tokens 23 | end 24 | tokens 25 | end 26 | 27 | def indent_text(indentation) 28 | children_copy = @children.clone 29 | i = 0 30 | while i < children_copy.size 31 | child = children_copy[i] 32 | if child.is_a?(Hocon::Impl::ConfigNodeSingleToken) && Hocon::Impl::Tokens.newline?(child.token) 33 | children_copy.insert(i + 1, indentation) 34 | i += 1 35 | elsif child.is_a?(Hocon::Impl::ConfigNodeField) 36 | value = child.value 37 | if value.is_a?(Hocon::Impl::ConfigNodeComplexValue) 38 | children_copy[i] = child.replace_value(value.indent_text(indentation)) 39 | end 40 | elsif child.is_a?(Hocon::Impl::ConfigNodeComplexValue) 41 | children_copy[i] = child.indent_text(indentation) 42 | end 43 | i += 1 44 | end 45 | new_node(children_copy) 46 | end 47 | 48 | # This method will just call into the object's constructor, but it's needed 49 | # for use in the indentText() method so we can avoid a gross if/else statement 50 | # checking the type of this 51 | def new_node(nodes) 52 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigNodeComplexValue should override `new_node` (#{self.class})" 53 | end 54 | end -------------------------------------------------------------------------------- /lib/hocon/impl/config_number.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_value' 5 | 6 | class Hocon::Impl::ConfigNumber 7 | include Hocon::Impl::AbstractConfigValue 8 | ## sigh... requiring these subclasses before this class 9 | ## is declared would cause an error. Thanks, ruby. 10 | require_relative '../../hocon/impl/config_int' 11 | require_relative '../../hocon/impl/config_double' 12 | 13 | def self.new_number(origin, number, original_text) 14 | as_int = number.to_i 15 | if as_int == number 16 | Hocon::Impl::ConfigInt.new(origin, as_int, original_text) 17 | else 18 | Hocon::Impl::ConfigDouble.new(origin, number, original_text) 19 | end 20 | end 21 | 22 | def initialize(origin, original_text) 23 | super(origin) 24 | @original_text = original_text 25 | end 26 | attr_reader :original_text 27 | 28 | def transform_to_string 29 | @original_text 30 | end 31 | 32 | def int_value_range_checked(path) 33 | # We don't need to do any range checking here due to the way Ruby handles 34 | # integers (doesn't have the 32-bit/64-bit distinction that Java does). 35 | long_value 36 | end 37 | 38 | def long_value 39 | raise "long_value needs to be overriden by sub-classes of #{Hocon::Impl::ConfigNumber}, in this case #{self.class}" 40 | end 41 | 42 | def can_equal(other) 43 | other.is_a?(Hocon::Impl::ConfigNumber) 44 | end 45 | 46 | def ==(other) 47 | if other.is_a?(Hocon::Impl::ConfigNumber) && can_equal(other) 48 | @value == other.value 49 | else 50 | false 51 | end 52 | end 53 | 54 | def hash 55 | # This hash function makes it so that a ConfigNumber with a 3.0 56 | # and one with a 3 will return the hash code 57 | to_int = @value.round 58 | 59 | # If the value is an integer or a floating point equal to an integer 60 | if to_int == @value 61 | to_int.hash 62 | else 63 | @value.hash 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/hocon/config_error.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | class Hocon::ConfigError < StandardError 6 | def initialize(origin, message, cause) 7 | msg = 8 | if origin.nil? 9 | message 10 | else 11 | "#{origin.description}: #{message}" 12 | end 13 | super(msg) 14 | @origin = origin 15 | @cause = cause 16 | end 17 | 18 | class ConfigMissingError < Hocon::ConfigError 19 | end 20 | 21 | class ConfigNullError < Hocon::ConfigError::ConfigMissingError 22 | def self.make_message(path, expected) 23 | if not expected.nil? 24 | "Configuration key '#{path}' is set to nil but expected #{expected}" 25 | else 26 | "Configuration key '#{path}' is nil" 27 | end 28 | end 29 | end 30 | 31 | class ConfigIOError < Hocon::ConfigError 32 | def initialize(origin, message, cause = nil) 33 | super(origin, message, cause) 34 | end 35 | end 36 | 37 | class ConfigParseError < Hocon::ConfigError 38 | end 39 | 40 | class ConfigWrongTypeError < Hocon::ConfigError 41 | def self.with_expected_actual(origin, path, expected, actual, cause = nil) 42 | ConfigWrongTypeError.new(origin, "#{path} has type #{actual} rather than #{expected}", cause) 43 | end 44 | end 45 | 46 | class ConfigBugOrBrokenError < Hocon::ConfigError 47 | def initialize(message, cause = nil) 48 | super(nil, message, cause) 49 | end 50 | end 51 | 52 | class ConfigNotResolvedError < Hocon::ConfigError::ConfigBugOrBrokenError 53 | end 54 | 55 | class ConfigBadPathError < Hocon::ConfigError 56 | def initialize(origin, path, message, cause = nil) 57 | error_message = !path.nil? ? "Invalid path '#{path}': #{message}" : message 58 | super(origin, error_message, cause) 59 | end 60 | end 61 | 62 | class UnresolvedSubstitutionError < ConfigParseError 63 | def initialize(origin, detail, cause = nil) 64 | super(origin, "Could not resolve substitution to a value: " + detail, cause) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/hocon/config_include_context.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_error' 5 | 6 | # 7 | # Context provided to a {@link ConfigIncluder}; this interface is only useful 8 | # inside a {@code ConfigIncluder} implementation, and is not intended for apps 9 | # to implement. 10 | # 11 | #

12 | # Do not implement this interface; it should only be implemented by 13 | # the config library. Arbitrary implementations will not work because the 14 | # library internals assume a specific concrete implementation. Also, this 15 | # interface is likely to grow new methods over time, so third-party 16 | # implementations will break. 17 | # 18 | module Hocon::ConfigIncludeContext 19 | # 20 | # Tries to find a name relative to whatever is doing the including, for 21 | # example in the same directory as the file doing the including. Returns 22 | # null if it can't meaningfully create a relative name. The returned 23 | # parseable may not exist; this function is not required to do any IO, just 24 | # compute what the name would be. 25 | # 26 | # The passed-in filename has to be a complete name (with extension), not 27 | # just a basename. (Include statements in config files are allowed to give 28 | # just a basename.) 29 | # 30 | # @param filename 31 | # the name to make relative to the resource doing the including 32 | # @return parseable item relative to the resource doing the including, or 33 | # null 34 | # 35 | def relative_to(filename) 36 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigIncludeContext` must implement `relative_to` (#{self.class})" 37 | end 38 | 39 | # 40 | # Parse options to use (if you use another method to get a 41 | # {@link ConfigParseable} then use {@link ConfigParseable#options()} 42 | # instead though). 43 | # 44 | # @return the parse options 45 | # 46 | def parse_options 47 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigIncludeContext` must implement `parse_options` (#{self.class})" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/hocon/config_parse_options.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | 5 | class Hocon::ConfigParseOptions 6 | attr_accessor :syntax, :origin_description, :allow_missing, :includer 7 | 8 | def self.defaults 9 | self.new(nil, nil, true, nil) 10 | end 11 | 12 | def initialize(syntax, origin_description, allow_missing, includer) 13 | @syntax = syntax 14 | @origin_description = origin_description 15 | @allow_missing = allow_missing 16 | @includer = includer 17 | end 18 | 19 | def set_syntax(syntax) 20 | if @syntax == syntax 21 | self 22 | else 23 | Hocon::ConfigParseOptions.new(syntax, 24 | @origin_description, 25 | @allow_missing, 26 | @includer) 27 | end 28 | end 29 | 30 | def set_origin_description(origin_description) 31 | if @origin_description == origin_description 32 | self 33 | else 34 | Hocon::ConfigParseOptions.new(@syntax, 35 | origin_description, 36 | @allow_missing, 37 | @includer) 38 | end 39 | end 40 | 41 | def set_allow_missing(allow_missing) 42 | if allow_missing? == allow_missing 43 | self 44 | else 45 | Hocon::ConfigParseOptions.new(@syntax, 46 | @origin_description, 47 | allow_missing, 48 | @includer) 49 | end 50 | end 51 | 52 | def allow_missing? 53 | @allow_missing 54 | end 55 | 56 | def set_includer(includer) 57 | if @includer == includer 58 | self 59 | else 60 | Hocon::ConfigParseOptions.new(@syntax, 61 | @origin_description, 62 | @allow_missing, 63 | includer) 64 | end 65 | end 66 | 67 | def append_includer(includer) 68 | if @includer == includer 69 | self 70 | elsif @includer 71 | set_includer(@includer.with_fallback(includer)) 72 | else 73 | set_includer(includer) 74 | end 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_string.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_value' 5 | require_relative '../../hocon/config_value_type' 6 | require_relative '../../hocon/impl/config_impl_util' 7 | 8 | class Hocon::Impl::ConfigString 9 | include Hocon::Impl::AbstractConfigValue 10 | 11 | ConfigImplUtil = Hocon::Impl::ConfigImplUtil 12 | 13 | attr_reader :value 14 | 15 | class Quoted < Hocon::Impl::ConfigString 16 | def initialize(origin, value) 17 | super(origin, value) 18 | end 19 | 20 | def new_copy(origin) 21 | self.class.new(origin, @value) 22 | end 23 | 24 | private 25 | 26 | # serialization all goes through SerializedConfigValue 27 | def write_replace 28 | Hocon::Impl::SerializedConfigValue.new(self) 29 | end 30 | end 31 | 32 | # this is sort of a hack; we want to preserve whether whitespace 33 | # was quoted until we process substitutions, so we can ignore 34 | # unquoted whitespace when concatenating lists or objects. 35 | # We dump this distinction when serializing and deserializing, 36 | # but that 's OK because it isn' t in equals/hashCode, and we 37 | # don 't allow serializing unresolved objects which is where 38 | # quoted-ness matters. If we later make ConfigOrigin point 39 | # to the original token range, we could use that to implement 40 | # wasQuoted() 41 | class Unquoted < Hocon::Impl::ConfigString 42 | def initialize(origin, value) 43 | super(origin, value) 44 | end 45 | 46 | def new_copy(origin) 47 | self.class.new(origin, @value) 48 | end 49 | 50 | def write_replace 51 | Hocon::Impl::SerializedConfigValue.new(self) 52 | end 53 | end 54 | 55 | def was_quoted? 56 | self.is_a?(Quoted) 57 | end 58 | 59 | def value_type 60 | Hocon::ConfigValueType::STRING 61 | end 62 | 63 | def unwrapped 64 | @value 65 | end 66 | 67 | def transform_to_string 68 | @value 69 | end 70 | 71 | def render_value_to_sb(sb, indent_size, at_root, options) 72 | if options.json? 73 | sb << ConfigImplUtil.render_json_string(@value) 74 | else 75 | sb << ConfigImplUtil.render_string_unquoted_if_possible(@value) 76 | end 77 | end 78 | 79 | private 80 | 81 | def initialize(origin, value) 82 | super(origin) 83 | @value = value 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_field.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/config_error' 5 | require_relative '../../hocon/impl/abstract_config_node' 6 | require_relative '../../hocon/impl/abstract_config_node_value' 7 | require_relative '../../hocon/impl/config_node_comment' 8 | require_relative '../../hocon/impl/config_node_path' 9 | require_relative '../../hocon/impl/config_node_single_token' 10 | require_relative '../../hocon/impl/tokens' 11 | 12 | class Hocon::Impl::ConfigNodeField 13 | include Hocon::Impl::AbstractConfigNode 14 | 15 | Tokens = Hocon::Impl::Tokens 16 | 17 | def initialize(children) 18 | @children = children 19 | end 20 | 21 | attr_reader :children 22 | 23 | def tokens 24 | tokens = [] 25 | @children.each do |child| 26 | tokens += child.tokens 27 | end 28 | tokens 29 | end 30 | 31 | def replace_value(new_value) 32 | children_copy = @children.clone 33 | children_copy.each_with_index do |child, i| 34 | if child.is_a?(Hocon::Impl::AbstractConfigNodeValue) 35 | children_copy[i] = new_value 36 | return self.class.new(children_copy) 37 | end 38 | end 39 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "Field node doesn't have a value" 40 | end 41 | 42 | def value 43 | @children.each do |child| 44 | if child.is_a?(Hocon::Impl::AbstractConfigNodeValue) 45 | return child 46 | end 47 | end 48 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "Field node doesn't have a value" 49 | end 50 | 51 | def path 52 | @children.each do |child| 53 | if child.is_a?(Hocon::Impl::ConfigNodePath) 54 | return child 55 | end 56 | end 57 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "Field node doesn't have a path" 58 | end 59 | 60 | def separator 61 | @children.each do |child| 62 | if child.is_a?(Hocon::Impl::ConfigNodeSingleToken) 63 | t = child.token 64 | if t == Tokens::PLUS_EQUALS or t == Tokens::COLON or t == Tokens::EQUALS 65 | return t 66 | end 67 | end 68 | end 69 | nil 70 | end 71 | 72 | def comments 73 | comments = [] 74 | @children.each do |child| 75 | if child.is_a?(Hocon::Impl::ConfigNodeComment) 76 | comments << child.comment_text 77 | end 78 | end 79 | comments 80 | end 81 | end -------------------------------------------------------------------------------- /lib/hocon.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Hocon 4 | # NOTE: the behavior of this load method differs a bit from the upstream public 5 | # API, where a file extension may be the preferred method of determining 6 | # the config syntax, even if you specify a Syntax value on ConfigParseOptions. 7 | # Here we prefer the syntax (optionally) specified by the user no matter what 8 | # the file extension is, and if they don't specify one and the file extension 9 | # is unrecognized, we raise an error. 10 | def self.load(file, opts = nil) 11 | # doing these requires lazily, because otherwise, classes that need to 12 | # `require_relative 'hocon'` to get the module into scope will end up recursing 13 | # through this require and probably ending up with circular dependencies. 14 | require_relative 'hocon/config_factory' 15 | require_relative 'hocon/impl/parseable' 16 | require_relative 'hocon/config_parse_options' 17 | require_relative 'hocon/config_resolve_options' 18 | require_relative 'hocon/config_error' 19 | syntax = opts ? opts[:syntax] : nil 20 | 21 | if syntax.nil? 22 | unless Hocon::Impl::Parseable.syntax_from_extension(file) 23 | raise Hocon::ConfigError::ConfigParseError.new( 24 | nil, "Unrecognized file extension '#{File.extname(file)}' and no value provided for :syntax option", nil) 25 | end 26 | config = Hocon::ConfigFactory.parse_file_any_syntax( 27 | file, Hocon::ConfigParseOptions.defaults) 28 | else 29 | config = Hocon::ConfigFactory.parse_file( 30 | file, Hocon::ConfigParseOptions.defaults.set_syntax(syntax)) 31 | end 32 | 33 | resolved_config = Hocon::ConfigFactory.load_from_config( 34 | config, Hocon::ConfigResolveOptions.defaults) 35 | 36 | resolved_config.root.unwrapped 37 | end 38 | 39 | def self.parse(string) 40 | # doing these requires lazily, because otherwise, classes that need to 41 | # `require_relative 'hocon'` to get the module into scope will end up recursing 42 | # through this require and probably ending up with circular dependencies. 43 | require_relative 'hocon/config_factory' 44 | require_relative 'hocon/config_resolve_options' 45 | config = Hocon::ConfigFactory.parse_string(string) 46 | resolved_config = Hocon::ConfigFactory.load_from_config( 47 | config, Hocon::ConfigResolveOptions.defaults) 48 | 49 | resolved_config.root.unwrapped 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/fixtures/test_utils/resources/test01.conf: -------------------------------------------------------------------------------- 1 | { 2 | "ints" : { 3 | "fortyTwo" : 42, 4 | "fortyTwoAgain" : ${ints.fortyTwo} 5 | }, 6 | 7 | "floats" : { 8 | "fortyTwoPointOne" : 42.1, 9 | "fortyTwoPointOneAgain" : ${floats.fortyTwoPointOne} 10 | }, 11 | 12 | "strings" : { 13 | "abcd" : "abcd", 14 | "abcdAgain" : ${strings.a}${strings.b}${strings.c}${strings.d}, 15 | "a" : "a", 16 | "b" : "b", 17 | "c" : "c", 18 | "d" : "d", 19 | "concatenated" : null bar 42 baz true 3.14 hi, 20 | "double" : "3.14", 21 | "number" : "57", 22 | "null" : "null", 23 | "true" : "true", 24 | "yes" : "yes", 25 | "false" : "false", 26 | "no" : "no" 27 | }, 28 | 29 | "arrays" : { 30 | "empty" : [], 31 | "ofInt" : [1, 2, 3], 32 | "ofString" : [ ${strings.a}, ${strings.b}, ${strings.c} ], 33 | "ofDouble" : [3.14, 4.14, 5.14], 34 | "ofNull" : [null, null, null], 35 | "ofBoolean" : [true, false], 36 | "ofArray" : [${arrays.ofString}, ${arrays.ofString}, ${arrays.ofString}], 37 | "ofObject" : [${ints}, ${booleans}, ${strings}], 38 | "firstElementNotASubst" : [ "a", ${strings.b} ] 39 | }, 40 | 41 | "booleans" : { 42 | "true" : true, 43 | "trueAgain" : ${booleans.true}, 44 | "false" : false, 45 | "falseAgain" : ${booleans.false} 46 | }, 47 | 48 | "nulls" : { 49 | "null" : null, 50 | "nullAgain" : ${nulls.null} 51 | }, 52 | 53 | "durations" : { 54 | "second" : 1s, 55 | "secondsList" : [1s,2seconds,3 s, 4000], 56 | "secondAsNumber" : 1000, 57 | "halfSecond" : 0.5s, 58 | "millis" : 1 milli, 59 | "micros" : 2000 micros 60 | }, 61 | 62 | "memsizes" : { 63 | "meg" : 1M, 64 | "megsList" : [1M, 1024K, 1048576], 65 | "megAsNumber" : 1048576, 66 | "halfMeg" : 0.5M 67 | }, 68 | 69 | "system" : { 70 | "javaversion" : ${?java.version}, 71 | "userhome" : ${?user.home}, 72 | "home" : ${?HOME}, 73 | "pwd" : ${?PWD}, 74 | "shell" : ${?SHELL}, 75 | "lang" : ${?LANG}, 76 | "path" : ${?PATH}, 77 | "not_here" : ${?NOT_HERE}, 78 | "concatenated" : Your Java version is ${?system.javaversion} and your user.home is ${?system.userhome} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_node_root.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_node_array' 5 | require_relative '../../hocon/impl/config_node_complex_value' 6 | require_relative '../../hocon/impl/config_node_object' 7 | 8 | class Hocon::Impl::ConfigNodeRoot 9 | include Hocon::Impl::ConfigNodeComplexValue 10 | def initialize(children, origin) 11 | super(children) 12 | @origin = origin 13 | end 14 | 15 | def new_node(nodes) 16 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "Tried to indent the root object" 17 | end 18 | 19 | def value 20 | @children.each do |node| 21 | if node.is_a?(Hocon::Impl::ConfigNodeComplexValue) 22 | return node 23 | end 24 | end 25 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "ConfigNodeRoot did not contain a value" 26 | end 27 | 28 | def set_value(desired_path, value, flavor) 29 | children_copy = @children.clone 30 | children_copy.each_with_index do |node, index| 31 | if node.is_a?(Hocon::Impl::ConfigNodeComplexValue) 32 | if node.is_a?(Hocon::Impl::ConfigNodeArray) 33 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "The ConfigDocument had an array at the root level, and values cannot be modified inside an array." 34 | elsif node.is_a?(Hocon::Impl::ConfigNodeObject) 35 | if value.nil? 36 | children_copy[index] = node.remove_value_on_path(desired_path, flavor) 37 | else 38 | children_copy[index] = node.set_value_on_path(desired_path, value, flavor) 39 | end 40 | return self.class.new(children_copy, @origin) 41 | end 42 | end 43 | end 44 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "ConfigNodeRoot did not contain a value" 45 | end 46 | 47 | def has_value(desired_path) 48 | path = Hocon::Impl::PathParser.parse_path(desired_path) 49 | @children.each do |node| 50 | if node.is_a?(Hocon::Impl::ConfigNodeComplexValue) 51 | if node.is_a?(Hocon::Impl::ConfigNodeArray) 52 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "The ConfigDocument had an array at the root level, and values cannot be modified inside an array." 53 | elsif node.is_a?(Hocon::Impl::ConfigNodeObject) 54 | return node.has_value(path) 55 | end 56 | end 57 | end 58 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "ConfigNodeRoot did not contain a value" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/hocon/config_util.rb: -------------------------------------------------------------------------------- 1 | require_relative '../hocon/impl/config_impl_util' 2 | 3 | 4 | # Contains static utility methods 5 | class Hocon::ConfigUtil 6 | # 7 | # Quotes and escapes a string, as in the JSON specification. 8 | # 9 | # @param string 10 | # a string 11 | # @return the string quoted and escaped 12 | # 13 | def self.quote_string(string) 14 | Hocon::Impl::ConfigImplUtil.render_json_string(string) 15 | end 16 | 17 | # 18 | # Converts a list of keys to a path expression, by quoting the path 19 | # elements as needed and then joining them separated by a period. A path 20 | # expression is usable with a {@link Config}, while individual path 21 | # elements are usable with a {@link ConfigObject}. 22 | #

23 | # See the overview documentation for {@link Config} for more detail on path 24 | # expressions vs. keys. 25 | # 26 | # @param elements 27 | # the keys in the path 28 | # @return a path expression 29 | # @throws ConfigException 30 | # if there are no elements 31 | # 32 | def self.join_path(*elements) 33 | Hocon::Impl::ConfigImplUtil.join_path(*elements) 34 | end 35 | 36 | # 37 | # Converts a list of strings to a path expression, by quoting the path 38 | # elements as needed and then joining them separated by a period. A path 39 | # expression is usable with a {@link Config}, while individual path 40 | # elements are usable with a {@link ConfigObject}. 41 | #

42 | # See the overview documentation for {@link Config} for more detail on path 43 | # expressions vs. keys. 44 | # 45 | # @param elements 46 | # the keys in the path 47 | # @return a path expression 48 | # @throws ConfigException 49 | # if the list is empty 50 | # 51 | def self.join_path_from_list(elements) 52 | self.join_path(*elements) 53 | end 54 | 55 | # 56 | # Converts a path expression into a list of keys, by splitting on period 57 | # and unquoting the individual path elements. A path expression is usable 58 | # with a {@link Config}, while individual path elements are usable with a 59 | # {@link ConfigObject}. 60 | #

61 | # See the overview documentation for {@link Config} for more detail on path 62 | # expressions vs. keys. 63 | # 64 | # @param path 65 | # a path expression 66 | # @return the individual keys in the path 67 | # @throws ConfigException 68 | # if the path expression is invalid 69 | # 70 | def self.split_path(path) 71 | Hocon::Impl::ConfigImplUtil.split_path(path) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/hocon/config_factory.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/impl/parseable' 5 | require_relative '../hocon/config_parse_options' 6 | require_relative '../hocon/impl/config_impl' 7 | require_relative '../hocon/config_factory' 8 | 9 | ## Please note that the `parse` operations will simply create a ConfigValue 10 | ## and do nothing else, whereas the `load` operations will perform a higher-level 11 | ## operation and will resolve substitutions. If you have substitutions in your 12 | ## configuration, use a `load` function 13 | class Hocon::ConfigFactory 14 | def self.parse_file(file_path, options = Hocon::ConfigParseOptions.defaults) 15 | Hocon::Impl::Parseable.new_file(file_path, options).parse.to_config 16 | end 17 | 18 | def self.parse_string(string, options = Hocon::ConfigParseOptions.defaults) 19 | Hocon::Impl::Parseable.new_string(string, options).parse.to_config 20 | end 21 | 22 | def self.parse_file_any_syntax(file_base_name, options) 23 | Hocon::Impl::ConfigImpl.parse_file_any_syntax(file_base_name, options).to_config 24 | end 25 | 26 | def self.empty(origin_description = nil) 27 | Hocon::Impl::ConfigImpl.empty_config(origin_description) 28 | end 29 | 30 | # Because of how optional arguments work, if either parse or resolve options is supplied 31 | # both must be supplied. load_file_with_parse_options or load_file_with_resolve_options 32 | # can be used instead, or the argument you don't care about in load_file can be nil 33 | # 34 | # e.g.: 35 | # load_file("settings", my_parse_options, nil) 36 | # is equivalent to: 37 | # load_file_with_parse_options("settings", my_parse_options) 38 | def self.load_file(file_base_name, parse_options = nil, resolve_options = nil) 39 | parse_options ||= Hocon::ConfigParseOptions.defaults 40 | resolve_options ||= Hocon::ConfigResolveOptions.defaults 41 | 42 | config = Hocon::ConfigFactory.parse_file_any_syntax(file_base_name, parse_options) 43 | 44 | self.load_from_config(config, resolve_options) 45 | end 46 | 47 | def self.load_file_with_parse_options(file_base_name, parse_options) 48 | self.load_file(file_base_name, parse_options, nil) 49 | end 50 | 51 | def self.load_file_with_resolve_options(file_base_name, resolve_options) 52 | self.load_file(file_base_name, nil, resolve_options) 53 | end 54 | 55 | def self.load_from_config(config, resolve_options) 56 | 57 | config.with_fallback(self.default_reference).resolve(resolve_options) 58 | end 59 | 60 | def self.default_reference 61 | Hocon::Impl::ConfigImpl.default_reference 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/hocon/config_mergeable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_error' 5 | 6 | # 7 | # Marker for types whose instances can be merged, that is {@link Config} and 8 | # {@link ConfigValue}. Instances of {@code Config} and {@code ConfigValue} can 9 | # be combined into a single new instance using the 10 | # {@link ConfigMergeable#withFallback withFallback()} method. 11 | # 12 | #

13 | # Do not implement this interface; it should only be implemented by 14 | # the config library. Arbitrary implementations will not work because the 15 | # library internals assume a specific concrete implementation. Also, this 16 | # interface is likely to grow new methods over time, so third-party 17 | # implementations will break. 18 | # 19 | module Hocon::ConfigMergeable 20 | # 21 | # Returns a new value computed by merging this value with another, with 22 | # keys in this value "winning" over the other one. 23 | # 24 | #

25 | # This associative operation may be used to combine configurations from 26 | # multiple sources (such as multiple configuration files). 27 | # 28 | #

29 | # The semantics of merging are described in the spec 31 | # for HOCON. Merging typically occurs when either the same object is 32 | # created twice in the same file, or two config files are both loaded. For 33 | # example: 34 | # 35 | #

36 | #  foo = { a: 42 }
37 | #  foo = { b: 43 }
38 | # 
39 | # 40 | # Here, the two objects are merged as if you had written: 41 | # 42 | #
43 | #  foo = { a: 42, b: 43 }
44 | # 
45 | # 46 | #

47 | # Only {@link ConfigObject} and {@link Config} instances do anything in 48 | # this method (they need to merge the fallback keys into themselves). All 49 | # other values just return the original value, since they automatically 50 | # override any fallback. This means that objects do not merge "across" 51 | # non-objects; if you write 52 | # object.withFallback(nonObject).withFallback(otherObject), 53 | # then otherObject will simply be ignored. This is an 54 | # intentional part of how merging works, because non-objects such as 55 | # strings and integers replace (rather than merging with) any prior value: 56 | # 57 | #

58 | # foo = { a: 42 }
59 | # foo = 10
60 | # 
61 | # 62 | # Here, the number 10 "wins" and the value of foo would be 63 | # simply 10. Again, for details see the spec. 64 | # 65 | # @param other 66 | # an object whose keys should be used as fallbacks, if the keys 67 | # are not present in this one 68 | # @return a new object (or the original one, if the fallback doesn't get 69 | # used) 70 | # 71 | def with_fallback(other) 72 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigMergeable` must implement `with_fallback` (#{self.class})" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_impl_util.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require 'stringio' 5 | 6 | class Hocon::Impl::ConfigImplUtil 7 | def self.equals_handling_nil?(a, b) 8 | # This method probably doesn't make any sense in ruby... not sure 9 | if a.nil? && !b.nil? 10 | false 11 | elsif !a.nil? && b.nil? 12 | false 13 | # in ruby, the == and .equal? are the opposite of what they are in Java 14 | elsif a.equal?(b) 15 | true 16 | else 17 | a == b 18 | end 19 | end 20 | 21 | # 22 | # This is public ONLY for use by the "config" package, DO NOT USE this ABI 23 | # may change. 24 | # 25 | def self.render_json_string(s) 26 | sb = StringIO.new 27 | sb << '"' 28 | s.chars.each do |c| 29 | case c 30 | when '"' then sb << "\\\"" 31 | when "\\" then sb << "\\\\" 32 | when "\n" then sb << "\\n" 33 | when "\b" then sb << "\\b" 34 | when "\f" then sb << "\\f" 35 | when "\r" then sb << "\\r" 36 | when "\t" then sb << "\\t" 37 | else 38 | if c =~ /[[:cntrl:]]/ 39 | sb << ("\\u%04x" % c) 40 | else 41 | sb << c 42 | end 43 | end 44 | end 45 | sb << '"' 46 | sb.string 47 | end 48 | 49 | def self.render_string_unquoted_if_possible(s) 50 | # this can quote unnecessarily as long as it never fails to quote when 51 | # necessary 52 | if s.length == 0 53 | return render_json_string(s) 54 | end 55 | 56 | # if it starts with a hyphen or number, we have to quote 57 | # to ensure we end up with a string and not a number 58 | first = s.chars.first 59 | if (first =~ /[[:digit:]]/) || (first == '-') 60 | return render_json_string(s) 61 | end 62 | 63 | # only unquote if it's pure alphanumeric 64 | s.chars.each do |c| 65 | unless (c =~ /[[:alnum:]]/) || (c == '-') 66 | return render_json_string(s) 67 | end 68 | end 69 | 70 | s 71 | end 72 | 73 | def self.join_path(*elements) 74 | Hocon::Impl::Path.from_string_list(elements).render 75 | end 76 | 77 | def self.split_path(path) 78 | p = Hocon::Impl::Path.new_path(path) 79 | elements = [] 80 | 81 | until p.nil? 82 | elements << p.first 83 | p = p.remainder 84 | end 85 | 86 | elements 87 | end 88 | 89 | def self.whitespace?(c) 90 | # this implementation is *not* a port of the java code, because it relied on 91 | # the method java.lang.Character#isWhitespace. This is probably 92 | # insanely slow (running a regex against every single character in the 93 | # file). 94 | c =~ /[[:space:]]/ 95 | end 96 | 97 | def self.unicode_trim(s) 98 | # this implementation is *not* a port of the java code. Ruby can strip 99 | # unicode whitespace much easier than Java can, and relies on a lot of 100 | # Java functions that don't really have straight equivalents in Ruby. 101 | s.gsub(/[:space]/, ' ') 102 | s.strip 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/hocon/config_value_factory.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/impl/config_impl' 5 | 6 | class Hocon::ConfigValueFactory 7 | ConfigImpl = Hocon::Impl::ConfigImpl 8 | 9 | # 10 | # Creates a {@link ConfigValue} from a plain value, which may be 11 | # a Boolean, Number, String, 12 | # Hash, or nil. A 13 | # Hash must be a Hash from String to more values 14 | # that can be supplied to from_any_ref(). A Hash 15 | # will become a {@link ConfigObject} and an Array will become a 16 | # {@link ConfigList}. 17 | # 18 | #

19 | # In a Hash passed to from_any_ref(), the map's keys 20 | # are plain keys, not path expressions. So if your Hash has a 21 | # key "foo.bar" then you will get one object with a key called "foo.bar", 22 | # rather than an object with a key "foo" containing another object with a 23 | # key "bar". 24 | # 25 | #

26 | # The origin_description will be used to set the origin() field on the 27 | # ConfigValue. It should normally be the name of the file the values came 28 | # from, or something short describing the value such as "default settings". 29 | # The origin_description is prefixed to error messages so users can tell 30 | # where problematic values are coming from. 31 | # 32 | #

33 | # Supplying the result of ConfigValue.unwrapped() to this function is 34 | # guaranteed to work and should give you back a ConfigValue that matches 35 | # the one you unwrapped. The re-wrapped ConfigValue will lose some 36 | # information that was present in the original such as its origin, but it 37 | # will have matching values. 38 | # 39 | #

40 | # If you pass in a ConfigValue to this 41 | # function, it will be returned unmodified. (The 42 | # origin_description will be ignored in this 43 | # case.) 44 | # 45 | #

46 | # This function throws if you supply a value that cannot be converted to a 47 | # ConfigValue, but supplying such a value is a bug in your program, so you 48 | # should never handle the exception. Just fix your program (or report a bug 49 | # against this library). 50 | # 51 | # @param object 52 | # object to convert to ConfigValue 53 | # @param origin_description 54 | # name of origin file or brief description of what the value is 55 | # @return a new value 56 | # 57 | def self.from_any_ref(object, origin_description = nil) 58 | if object.is_a?(Hash) 59 | from_map(object, origin_description) 60 | else 61 | ConfigImpl.from_any_ref(object, origin_description) 62 | end 63 | end 64 | 65 | # 66 | # See the {@link #from_any_ref(Object,String)} documentation for details 67 | # 68 | #

69 | # See also {@link ConfigFactory#parse_map(Map)} which interprets the keys in 70 | # the map as path expressions. 71 | # 72 | # @param values map from keys to plain ruby values 73 | # @return a new {@link ConfigObject} 74 | # 75 | def self.from_map(values, origin_description = nil) 76 | ConfigImpl.from_any_ref(process_hash(values), origin_description) 77 | end 78 | 79 | private 80 | 81 | def self.process_hash(hash) 82 | Hash[hash.map {|k, v| [k.is_a?(Symbol) ? k.to_s : k, v.is_a?(Hash) ? process_hash(v) : v]}] 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /spec/unit/typesafe/config/config_value_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon/config_value_factory' 5 | require 'hocon/config_render_options' 6 | require 'hocon/config_error' 7 | 8 | describe Hocon::ConfigValueFactory do 9 | let(:render_options) { Hocon::ConfigRenderOptions.defaults } 10 | 11 | before do 12 | render_options.origin_comments = false 13 | render_options.json = false 14 | end 15 | 16 | context "converting objects to ConfigValue using ConfigValueFactory" do 17 | it "should convert true into a ConfigBoolean" do 18 | value = Hocon::ConfigValueFactory.from_any_ref(true, nil) 19 | expect(value).to be_instance_of(Hocon::Impl::ConfigBoolean) 20 | expect(value.unwrapped).to eql(true) 21 | end 22 | 23 | it "should convert false into a ConfigBoolean" do 24 | value = Hocon::ConfigValueFactory.from_any_ref(false, nil) 25 | expect(value).to be_instance_of(Hocon::Impl::ConfigBoolean) 26 | expect(value.unwrapped).to eql(false) 27 | end 28 | 29 | it "should convert nil into a ConfigNull object" do 30 | value = Hocon::ConfigValueFactory.from_any_ref(nil, nil) 31 | expect(value).to be_instance_of(Hocon::Impl::ConfigNull) 32 | expect(value.unwrapped).to be_nil 33 | end 34 | 35 | it "should convert an string into a ConfigString object" do 36 | value = Hocon::ConfigValueFactory.from_any_ref("Hello, World!", nil) 37 | expect(value).to be_a(Hocon::Impl::ConfigString) 38 | expect(value.unwrapped).to eq("Hello, World!") 39 | end 40 | 41 | it "should convert an integer into a ConfigInt object" do 42 | value = Hocon::ConfigValueFactory.from_any_ref(123, nil) 43 | expect(value).to be_instance_of(Hocon::Impl::ConfigInt) 44 | expect(value.unwrapped).to eq(123) 45 | end 46 | 47 | it "should convert a double into a ConfigDouble object" do 48 | value = Hocon::ConfigValueFactory.from_any_ref(123.456, nil) 49 | expect(value).to be_instance_of(Hocon::Impl::ConfigDouble) 50 | expect(value.unwrapped).to eq(123.456) 51 | end 52 | 53 | it "should convert a map into a SimpleConfigObject" do 54 | map = {"a" => 1, "b" => 2, "c" => 3} 55 | value = Hocon::ConfigValueFactory.from_any_ref(map, nil) 56 | expect(value).to be_instance_of(Hocon::Impl::SimpleConfigObject) 57 | expect(value.unwrapped).to eq(map) 58 | end 59 | 60 | it "should convert symbol keys in a map to string keys" do 61 | orig_map = {a: 1, b: 2, c: {a: 1, b: 2, c: {a: 1}}} 62 | map = {"a" => 1, "b" => 2, "c"=>{"a"=>1, "b"=>2, "c"=>{"a"=>1}}} 63 | value = Hocon::ConfigValueFactory.from_any_ref(orig_map, nil) 64 | expect(value).to be_instance_of(Hocon::Impl::SimpleConfigObject) 65 | expect(value.unwrapped).to eq(map) 66 | 67 | value = Hocon::ConfigValueFactory.from_map(orig_map, nil) 68 | expect(value).to be_instance_of(Hocon::Impl::SimpleConfigObject) 69 | expect(value.unwrapped).to eq(map) 70 | end 71 | 72 | it "should not parse maps with non-string and non-symbol keys" do 73 | map = {1 => "a", 2 => "b"} 74 | expect{ Hocon::ConfigValueFactory.from_any_ref(map, nil) }.to raise_error(Hocon::ConfigError::ConfigBugOrBrokenError) 75 | end 76 | 77 | it "should convert an Enumerable into a SimpleConfigList" do 78 | list = [1, 2, 3, 4, 5] 79 | value = Hocon::ConfigValueFactory.from_any_ref(list, nil) 80 | expect(value).to be_instance_of(Hocon::Impl::SimpleConfigList) 81 | expect(value.unwrapped).to eq(list) 82 | end 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.1 2 | This is a bugfix release 3 | 4 | * Fix a bug when using the library in multiple threads ([HC-105](https://tickets.puppetlabs.com/browse/HC-105)) 5 | 6 | ## 1.3.0 7 | This is a feature release 8 | 9 | * Support environment variable lists ([HC-104](https://tickets.puppetlabs.com/browse/HC-104)) 10 | 11 | ## 1.2.6 12 | This is a bugfix release 13 | 14 | * Do not ship spec folder with gem ([PA-2942](https://tickets.puppetlabs.com/browse/PA-2942)) 15 | 16 | ## 1.2.5 17 | This is a bugfix release 18 | 19 | * Fixed loading files with UTF-8 characters in their file paths 20 | 21 | ## 1.2.4 22 | This is a feature release. 23 | 24 | * Added a cli tool called `hocon` for reading and manipulating hocon files 25 | 26 | Note that the version numbers 1.2.0-1.2.3 were not used because of bugs in our 27 | release pipeline we were working out 28 | 29 | ## 1.1.3 30 | This is a bugfix release. 31 | 32 | * Fixed bug where Hocon.parse would throw a ConfigNotResolved error if you passed it a String 33 | that contained values with substitutions. 34 | 35 | ## 1.1.2 36 | This is a bugfix release. 37 | 38 | * Fixed bug where Hocon::ConfigFactory.parse_file was not handling files with BOMs on Windows, 39 | causing UTF-8 files to not load properly. 40 | 41 | ## 1.1.1 42 | This is a bugfix release. 43 | 44 | * Fixed a bug where an undefined method `value_type_name` error was being thrown due to 45 | improper calls to the class method. 46 | 47 | ## 1.1.0 48 | This is a bugfix/feature release 49 | 50 | * Fixed a bug where unrecognized config file extensions caused `Hocon.load` to return an empty 51 | hash instead of an error. 52 | * Added an optional `:syntax` key to the `Hocon.load` method to explicitly specify the file format 53 | * Renamed internal usage of `name` methods to avoid overriding built in `Object#name` method 54 | 55 | ## 1.0.1 56 | 57 | This is a bugfix release. 58 | The API is stable enough and the code is being used in production, so the version is also being bumped to 1.0.0 59 | 60 | * Fixed a bug wherein calling "Hocon.load" would not 61 | resolve substitutions. 62 | * Fixed a circular dependency between the Hocon and Hocon::ConfigFactory 63 | namespaces. Using the Hocon::ConfigFactory class now requires you to 64 | use a `require 'hocon/config_factory'` instead of `require hocon` 65 | * Add support for hashes with keyword keys 66 | 67 | ## 1.0.0 68 | 69 | This version number was burned. 70 | 71 | ## 0.9.3 72 | 73 | This is a bugfix release. 74 | 75 | * Fixed a bug wherein inserting an array or a hash into a ConfigDocument would cause 76 | "# hardcoded value" comments to be generated before every entry in the hash/array. 77 | 78 | ## 0.9.2 79 | 80 | This is a bugfix release 81 | 82 | * Fixed a bug wherein attempting to insert a complex value (such as an array or a hash) into an empty 83 | ConfigDocument would cause an undefined method error. 84 | 85 | ## 0.9.1 86 | 87 | This is a bugfix release. 88 | * Fixed a bug wherein ugly configurations were being generated due to the addition of new objects when a setting 89 | is set at a path that does not currently exist in the configuration. Previously, these new objects were being 90 | added as single-line objects. They will now be added as multi-line objects if the parent object is a multi-line 91 | object or is an empty root object. 92 | 93 | ## 0.9.0 94 | 95 | This is a promotion of the 0.1.0 release with one small bug fix: 96 | * Fixed bug wherein using the `set_config_value` method with some parsed values would cause a failure due to surrounding whitespace 97 | 98 | ## 0.1.0 99 | 100 | This is a feature release containing a large number of changes and improvements 101 | 102 | * Added support for concatenation 103 | * Added support for substitutions 104 | * Added support for file includes. Other types of includes are not supported 105 | * Added the new ConfigDocument API that was recently implemented in the upstream Java library 106 | * Improved JSON support 107 | * Fixed a large number of small bugs related to various pieces of implementation 108 | -------------------------------------------------------------------------------- /spec/unit/typesafe/config/config_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon/config_factory' 5 | require 'hocon/config_render_options' 6 | require 'hocon/config_error' 7 | 8 | def get_comment_config_hash(config_string) 9 | split_config_string = config_string.split("\n") 10 | r = Regexp.new('^\s*#') 11 | 12 | previous_string_comment = false 13 | hash = {} 14 | comment_list = [] 15 | 16 | split_config_string.each do |s| 17 | if r.match(s) 18 | comment_list << s 19 | previous_string_comment = true 20 | else 21 | if previous_string_comment 22 | hash[s] = comment_list 23 | comment_list = [] 24 | end 25 | previous_string_comment = false 26 | end 27 | end 28 | return hash 29 | end 30 | 31 | describe Hocon::ConfigFactory do 32 | let(:render_options) { Hocon::ConfigRenderOptions.defaults } 33 | 34 | before do 35 | render_options.origin_comments = false 36 | render_options.json = false 37 | end 38 | 39 | shared_examples_for "config_factory_parsing" do 40 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input#{extension}" } 41 | let(:output_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/output.conf" } 42 | let(:expected) { example[:hash] } 43 | let(:reparsed) { Hocon::ConfigFactory.parse_file("#{output_file}") } 44 | let(:output) { File.read("#{output_file}") } 45 | 46 | it "should make the config data available as a map" do 47 | expect(conf.root.unwrapped).to eq(expected) 48 | end 49 | 50 | it "should render the config data to a string with comments intact" do 51 | rendered_conf = conf.root.render(render_options) 52 | rendered_conf_comment_hash = get_comment_config_hash(rendered_conf) 53 | output_comment_hash = get_comment_config_hash(output) 54 | 55 | expect(rendered_conf_comment_hash).to eq(output_comment_hash) 56 | end 57 | 58 | it "should generate the same conf data via re-parsing the rendered output" do 59 | expect(reparsed.root.unwrapped).to eq(expected) 60 | end 61 | end 62 | 63 | context "example1" do 64 | let(:example) { EXAMPLE1 } 65 | let (:extension) { ".conf" } 66 | 67 | context "parsing a HOCON string" do 68 | let(:string) { File.open(input_file).read } 69 | let(:conf) { Hocon::ConfigFactory.parse_string(string) } 70 | include_examples "config_factory_parsing" 71 | end 72 | 73 | context "parsing a .conf file" do 74 | let(:conf) { Hocon::ConfigFactory.parse_file(input_file) } 75 | include_examples "config_factory_parsing" 76 | end 77 | end 78 | 79 | context "example2" do 80 | let(:example) { EXAMPLE2 } 81 | let (:extension) { ".conf" } 82 | 83 | context "parsing a HOCON string" do 84 | let(:string) { File.open(input_file).read } 85 | let(:conf) { Hocon::ConfigFactory.parse_string(string) } 86 | include_examples "config_factory_parsing" 87 | end 88 | 89 | context "parsing a .conf file" do 90 | let(:conf) { Hocon::ConfigFactory.parse_file(input_file) } 91 | include_examples "config_factory_parsing" 92 | end 93 | end 94 | 95 | context "example3" do 96 | let (:example) { EXAMPLE3 } 97 | let (:extension) { ".conf" } 98 | 99 | context "loading a HOCON file with substitutions" do 100 | let(:conf) { Hocon::ConfigFactory.load_file(input_file) } 101 | include_examples "config_factory_parsing" 102 | end 103 | end 104 | 105 | context "example4" do 106 | let(:example) { EXAMPLE4 } 107 | let (:extension) { ".json" } 108 | 109 | context "parsing a .json file" do 110 | let (:conf) { Hocon::ConfigFactory.parse_file(input_file) } 111 | include_examples "config_factory_parsing" 112 | end 113 | end 114 | 115 | context "example5" do 116 | it "should raise a ConfigParseError when given an invalid .conf file" do 117 | expect{Hocon::ConfigFactory.parse_string("abcdefg")}.to raise_error(Hocon::ConfigError::ConfigParseError) 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/hocon/impl/default_transformer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/config_string' 5 | require_relative '../../hocon/config_value_type' 6 | require_relative '../../hocon/impl/config_boolean' 7 | 8 | class Hocon::Impl::DefaultTransformer 9 | 10 | ConfigValueType = Hocon::ConfigValueType 11 | ConfigString = Hocon::Impl::ConfigString 12 | ConfigBoolean = Hocon::Impl::ConfigBoolean 13 | 14 | def self.transform(value, requested) 15 | if value.value_type == ConfigValueType::STRING 16 | s = value.unwrapped 17 | case requested 18 | when ConfigValueType::NUMBER 19 | begin 20 | v = Integer(s) 21 | return ConfigInt.new(value.origin, v, s) 22 | rescue ArgumentError 23 | # try Float 24 | end 25 | begin 26 | v = Float(s) 27 | return ConfigFloat.new(value.origin, v, s) 28 | rescue ArgumentError 29 | # oh well. 30 | end 31 | when ConfigValueType::NULL 32 | if s == "null" 33 | return ConfigNull.new(value.origin) 34 | end 35 | when ConfigValueType::BOOLEAN 36 | if s == "true" || s == "yes" || s == "on" 37 | return ConfigBoolean.new(value.origin, true) 38 | elsif s == "false" || s == "no" || s == "off" 39 | return ConfigBoolean.new(value.origin, false) 40 | end 41 | when ConfigValueType::LIST 42 | # can't go STRING to LIST automatically 43 | when ConfigValueType::OBJECT 44 | # can't go STRING to OBJECT automatically 45 | when ConfigValueType::STRING 46 | # no-op STRING to STRING 47 | end 48 | elsif requested == ConfigValueType::STRING 49 | # if we converted null to string here, then you wouldn't properly 50 | # get a missing-value error if you tried to get a null value 51 | # as a string. 52 | case value.value_type 53 | # Ruby note: can't fall through in ruby. In the java code, NUMBER 54 | # just rolls over to the BOOLEAN case 55 | when ConfigValueType::NUMBER 56 | return ConfigString::Quoted.new(value.origin, value.transform_to_string) 57 | when ConfigValueType::BOOLEAN 58 | return ConfigString::Quoted.new(value.origin, value.transform_to_string) 59 | when ConfigValueType::NULL 60 | # want to be sure this throws instead of returning "null" as a 61 | # string 62 | when ConfigValueType::OBJECT 63 | # no OBJECT to STRING automatically 64 | when ConfigValueType::LIST 65 | # no LIST to STRING automatically 66 | when ConfigValueType::STRING 67 | # no-op STRING to STRING 68 | end 69 | elsif requested == ConfigValueType::LIST && value.value_type == ConfigValueType::OBJECT 70 | # attempt to convert an array-like (numeric indices) object to a 71 | # list. This would be used with .properties syntax for example: 72 | # -Dfoo.0=bar -Dfoo.1=baz 73 | # To ensure we still throw type errors for objects treated 74 | # as lists in most cases, we'll refuse to convert if the object 75 | # does not contain any numeric keys. This means we don't allow 76 | # empty objects here though :-/ 77 | o = value 78 | values = Hash.new 79 | values 80 | o.keys.each do |key| 81 | begin 82 | i = Integer(key, 10) 83 | if i < 0 84 | next 85 | end 86 | values[key] = i 87 | rescue ArgumentError 88 | next 89 | end 90 | end 91 | if not values.empty? 92 | entry_list = values.to_a 93 | # sort by numeric index 94 | entry_list.sort! {|a,b| b[0] <=> a[0]} 95 | # drop the indices (we allow gaps in the indices, for better or 96 | # worse) 97 | list = Array.new 98 | entry_list.each do |entry| 99 | list.push(entry[1]) 100 | end 101 | return SimpleConfigList.new(value.origin, list) 102 | end 103 | end 104 | value 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/hocon/parser/config_document.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/parser' 4 | require_relative '../../hocon/config_error' 5 | 6 | # 7 | # Represents an individual HOCON or JSON file, preserving all 8 | # formatting and syntax details. This can be used to replace 9 | # individual values and exactly render the original text of the 10 | # input. 11 | # 12 | #

13 | # Because this object is immutable, it is safe to use from multiple threads and 14 | # there's no need for "defensive copies." 15 | # 16 | #

17 | # Do not implement interface {@code ConfigDocument}; it should only be 18 | # implemented by the config library. Arbitrary implementations will not work 19 | # because the library internals assume a specific concrete implementation.# 20 | # Also, this interface is likely to grow new methods over time, so third-party 21 | # implementations will break. 22 | # 23 | 24 | module Hocon::Parser::ConfigDocument 25 | # 26 | # Returns a new ConfigDocument that is a copy of the current ConfigDocument, 27 | # but with the desired value set at the desired path. If the path exists, it will 28 | # remove all duplicates before the final occurrence of the path, and replace the value 29 | # at the final occurrence of the path. If the path does not exist, it will be added. If 30 | # the document has an array as the root value, an exception will be thrown. 31 | # 32 | # @param path the path at which to set the desired value 33 | # @param newValue the value to set at the desired path, represented as a string. This 34 | # string will be parsed into a ConfigNode using the same options used to 35 | # parse the entire document, and the text will be inserted 36 | # as-is into the document. Leading and trailing comments, whitespace, or 37 | # newlines are not allowed, and if present an exception will be thrown. 38 | # If a concatenation is passed in for newValue but the document was parsed 39 | # with JSON, the first value in the concatenation will be parsed and inserted 40 | # into the ConfigDocument. 41 | # @return a copy of the ConfigDocument with the desired value at the desired path 42 | # 43 | def set_value(path, new_value) 44 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigDocument should override `render` (#{self.class})" 45 | end 46 | 47 | # 48 | # Returns a new ConfigDocument that is a copy of the current ConfigDocument, 49 | # but with the desired value set at the desired path as with {@link #setValue(String, String)}, 50 | # but takes a ConfigValue instead of a string. 51 | # 52 | # @param path the path at which to set the desired value 53 | # @param newValue the value to set at the desired path, represented as a ConfigValue. 54 | # The rendered text of the ConfigValue will be inserted into the 55 | # ConfigDocument. 56 | # @return a copy of the ConfigDocument with the desired value at the desired path 57 | # 58 | def set_config_value(path, new_value) 59 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigDocument should override `render` (#{self.class})" 60 | end 61 | 62 | # 63 | # Returns a new ConfigDocument that is a copy of the current ConfigDocument, but with 64 | # the value at the desired path removed. If the desired path does not exist in the document, 65 | # a copy of the current document will be returned. If there is an array at the root, an exception 66 | # will be thrown. 67 | # 68 | # @param path the path to remove from the document 69 | # @return a copy of the ConfigDocument with the desired value removed from the document. 70 | # 71 | def remove_value(path) 72 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigDocument should override `render` (#{self.class})" 73 | end 74 | 75 | # 76 | # Returns a boolean indicating whether or not a ConfigDocument has a value at the desired path. 77 | # @param path the path to check 78 | # @return true if the path exists in the document, otherwise false 79 | # 80 | def has_value?(path) 81 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigDocument should override `render` (#{self.class})" 82 | end 83 | 84 | # 85 | # The original text of the input, modified if necessary with 86 | # any replaced or added values. 87 | # @return the modified original text 88 | # 89 | def render 90 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigDocument should override `render` (#{self.class})" 91 | end 92 | end -------------------------------------------------------------------------------- /spec/unit/hocon/hocon_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon' 5 | require 'hocon/config_render_options' 6 | require 'hocon/config_error' 7 | require 'hocon/config_syntax' 8 | 9 | ConfigParseError = Hocon::ConfigError::ConfigParseError 10 | ConfigWrongTypeError = Hocon::ConfigError::ConfigWrongTypeError 11 | 12 | describe Hocon do 13 | let(:render_options) { Hocon::ConfigRenderOptions.defaults } 14 | 15 | before do 16 | render_options.origin_comments = false 17 | render_options.json = false 18 | end 19 | 20 | RSpec.shared_examples "hocon_parsing" do 21 | 22 | it "should make the config data available as a map" do 23 | expect(conf).to eq(expected) 24 | end 25 | 26 | end 27 | 28 | [EXAMPLE1, EXAMPLE2].each do |example| 29 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 30 | let(:output_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/output.conf" } 31 | let(:output) { File.read("#{output_file}") } 32 | let(:output_nocomments_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/output_nocomments.conf" } 33 | let(:output_nocomments) { File.read("#{output_nocomments_file}") } 34 | let(:expected) { example[:hash] } 35 | # TODO 'reparsed' appears to be unused 36 | let(:reparsed) { Hocon::ConfigFactory.parse_file("#{FIXTURE_DIR}/parse_render/#{example[:name]}/output.conf") } 37 | 38 | context "loading a HOCON file" do 39 | let(:conf) { Hocon.load(input_file) } 40 | include_examples "hocon_parsing" 41 | end 42 | 43 | context "parsing a HOCON string" do 44 | let(:string) { File.open(input_file).read } 45 | let(:conf) { Hocon.parse(string) } 46 | include_examples "hocon_parsing" 47 | end 48 | end 49 | 50 | it "should fail to parse an array" do 51 | puts 52 | expect{(Hocon.parse('[1,2,3]'))}. 53 | to raise_error(ConfigWrongTypeError) 54 | end 55 | 56 | it "should fail to parse an array" do 57 | expect{(Hocon.parse('["one", "two" "three"]'))}. 58 | to raise_error(ConfigWrongTypeError) 59 | end 60 | 61 | context "loading a HOCON file with a substitution" do 62 | conf = Hocon.load("#{FIXTURE_DIR}/parse_render/#{EXAMPLE3[:name]}/input.conf") 63 | expected = EXAMPLE3[:hash] 64 | it "should successfully resolve the substitution" do 65 | expect(conf).to eq(expected) 66 | end 67 | end 68 | 69 | context "loading a file with an unknown extension" do 70 | context "without specifying the config format" do 71 | it "should raise an error" do 72 | expect { 73 | Hocon.load("#{FIXTURE_DIR}/hocon/by_extension/cat.test") 74 | }.to raise_error(ConfigParseError, /Unrecognized file extension '.test'/) 75 | end 76 | end 77 | 78 | context "while specifying the config format" do 79 | it "should parse properly if the config format is correct" do 80 | expect(Hocon.load("#{FIXTURE_DIR}/hocon/by_extension/cat.test", 81 | {:syntax => Hocon::ConfigSyntax::HOCON})). 82 | to eq({"meow" => "cats"}) 83 | expect(Hocon.load("#{FIXTURE_DIR}/hocon/by_extension/cat.test-json", 84 | {:syntax => Hocon::ConfigSyntax::HOCON})). 85 | to eq({"meow" => "cats"}) 86 | end 87 | it "should parse properly if the config format is compatible" do 88 | expect(Hocon.load("#{FIXTURE_DIR}/hocon/by_extension/cat.test-json", 89 | {:syntax => Hocon::ConfigSyntax::JSON})). 90 | to eq({"meow" => "cats"}) 91 | end 92 | it "should raise an error if the config format is incompatible" do 93 | expect { 94 | Hocon.load("#{FIXTURE_DIR}/hocon/by_extension/cat.test", 95 | {:syntax => Hocon::ConfigSyntax::JSON}) 96 | }.to raise_error(ConfigParseError, /Document must have an object or array at root/) 97 | end 98 | end 99 | end 100 | 101 | context "loading config that includes substitutions" do 102 | it "should be able to `load` from a file" do 103 | expect(Hocon.load("#{FIXTURE_DIR}/hocon/with_substitution/subst.conf")). 104 | to eq({"a" => true, "b" => true, "c" => ["foo", "bar", "baz"]}) 105 | end 106 | it "should be able to `parse` from a string" do 107 | expect(Hocon.parse(File.read("#{FIXTURE_DIR}/hocon/with_substitution/subst.conf"))). 108 | to eq({"a" => true, "b" => true, "c" => ["foo", "bar", "baz"]}) 109 | end 110 | end 111 | 112 | 113 | end 114 | 115 | -------------------------------------------------------------------------------- /lib/hocon/config_value.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_mergeable' 5 | 6 | # 7 | # An immutable value, following the JSON type 8 | # schema. 9 | # 10 | #

11 | # Because this object is immutable, it is safe to use from multiple threads and 12 | # there's no need for "defensive copies." 13 | # 14 | #

15 | # Do not implement interface {@code ConfigValue}; it should only be 16 | # implemented by the config library. Arbitrary implementations will not work 17 | # because the library internals assume a specific concrete implementation. 18 | # Also, this interface is likely to grow new methods over time, so third-party 19 | # implementations will break. 20 | # 21 | module Hocon::ConfigValue 22 | include Hocon::ConfigMergeable 23 | 24 | # 25 | # The origin of the value (file, line number, etc.), for debugging and 26 | # error messages. 27 | # 28 | # @return where the value came from 29 | # 30 | def origin 31 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `origin` (#{self.class})" 32 | end 33 | 34 | 35 | # 36 | # The {@link ConfigValueType} of the value; matches the JSON type schema. 37 | # 38 | # @return value's type 39 | # 40 | def value_type 41 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `value_type` (#{self.class})" 42 | end 43 | 44 | # 45 | # Returns the value as a plain Java boxed value, that is, a {@code String}, 46 | # {@code Number}, {@code Boolean}, {@code Map}, 47 | # {@code List}, or {@code null}, matching the {@link #valueType()} 48 | # of this {@code ConfigValue}. If the value is a {@link ConfigObject} or 49 | # {@link ConfigList}, it is recursively unwrapped. 50 | # @return a plain Java value corresponding to this ConfigValue 51 | # 52 | def unwrapped 53 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `unwrapped` (#{self.class})" 54 | end 55 | 56 | # 57 | # Renders the config value to a string, using the provided options. 58 | # 59 | #

60 | # If the config value has not been resolved (see {@link Config#resolve}), 61 | # it's possible that it can't be rendered as valid HOCON. In that case the 62 | # rendering should still be useful for debugging but you might not be able 63 | # to parse it. If the value has been resolved, it will always be parseable. 64 | # 65 | #

66 | # If the config value has been resolved and the options disable all 67 | # HOCON-specific features (such as comments), the rendering will be valid 68 | # JSON. If you enable HOCON-only features such as comments, the rendering 69 | # will not be valid JSON. 70 | # 71 | # @param options 72 | # the rendering options 73 | # @return the rendered value 74 | # 75 | def render(options) 76 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `render` (#{self.class})" 77 | end 78 | 79 | def with_fallback(other) 80 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `with_fallback` (#{self.class})" 81 | end 82 | 83 | # 84 | # Places the value inside a {@link Config} at the given path. See also 85 | # {@link ConfigValue#atKey(String)}. 86 | # 87 | # @param path 88 | # path to store this value at. 89 | # @return a {@code Config} instance containing this value at the given 90 | # path. 91 | # 92 | def at_path(path) 93 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `at_path` (#{self.class})" 94 | end 95 | 96 | # 97 | # Places the value inside a {@link Config} at the given key. See also 98 | # {@link ConfigValue#atPath(String)}. 99 | # 100 | # @param key 101 | # key to store this value at. 102 | # @return a {@code Config} instance containing this value at the given key. 103 | # 104 | def at_key(key) 105 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `at_key` (#{self.class})" 106 | end 107 | 108 | # 109 | # Returns a {@code ConfigValue} based on this one, but with the given 110 | # origin. This is useful when you are parsing a new format of file or setting 111 | # comments for a single ConfigValue. 112 | # 113 | # @since 1.3.0 114 | # 115 | # @param origin the origin set on the returned value 116 | # @return the new ConfigValue with the given origin 117 | # 118 | def with_origin(origin) 119 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `ConfigValue` must implement `with_origin` (#{self.class})" 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /spec/unit/typesafe/config/simple_config_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon/config_factory' 5 | require 'hocon/config_render_options' 6 | require 'hocon/config_value_factory' 7 | 8 | describe Hocon::Impl::SimpleConfig do 9 | let(:render_options) { Hocon::ConfigRenderOptions.defaults } 10 | 11 | before do 12 | render_options.origin_comments = false 13 | render_options.json = false 14 | end 15 | 16 | shared_examples_for "config_value_retrieval_single_value" do 17 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 18 | it "should allow you to get a value for a specific configuration setting" do 19 | expect(conf.get_value(setting).transform_to_string).to eq(expected_setting) 20 | end 21 | end 22 | 23 | shared_examples_for "config_value_retrieval_config_list" do 24 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 25 | it "should allow you to get a value for a setting whose value is a data structure" do 26 | expect(conf.get_value(setting). 27 | render_value_to_sb(StringIO.new, 2, nil, 28 | Hocon::ConfigRenderOptions.new(false, false, false, false)). 29 | string).to eq(expected_setting) 30 | end 31 | end 32 | 33 | shared_examples_for "has_path_check" do 34 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 35 | it "should return true if a path exists" do 36 | expect(conf.has_path?(setting)).to eql(true) 37 | end 38 | 39 | it "should return false if a path does not exist" do 40 | expect(conf.has_path?(false_setting)).to eq(false) 41 | end 42 | end 43 | 44 | shared_examples_for "add_value_to_config" do 45 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 46 | it "should add desired setting with desired value" do 47 | modified_conf = conf.with_value(setting_to_add, value_to_add) 48 | expect(modified_conf.get_value(setting_to_add)).to eq(value_to_add) 49 | end 50 | end 51 | 52 | shared_examples_for "add_data_structures_to_config" do 53 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 54 | it "should add a nested map to a config" do 55 | map = Hocon::ConfigValueFactory.from_any_ref({"a" => "b", "c" => {"d" => "e"}}, nil) 56 | modified_conf = conf.with_value(setting_to_add, map) 57 | expect(modified_conf.get_value(setting_to_add)).to eq(map) 58 | end 59 | 60 | it "should add an array to a config" do 61 | array = Hocon::ConfigValueFactory.from_any_ref([1,2,3,4,5], nil) 62 | modified_conf = conf.with_value(setting_to_add, array) 63 | expect(modified_conf.get_value(setting_to_add)).to eq(array) 64 | end 65 | end 66 | 67 | shared_examples_for "remove_value_from_config" do 68 | let(:input_file) { "#{FIXTURE_DIR}/parse_render/#{example[:name]}/input.conf" } 69 | it "should remove desired setting" do 70 | modified_conf = conf.without_path(setting_to_remove) 71 | expect(modified_conf.has_path?(setting_to_remove)).to be false 72 | end 73 | end 74 | 75 | context "example1" do 76 | let(:example) { EXAMPLE1 } 77 | let(:setting) { "foo.bar.yahoo" } 78 | let(:expected_setting) { "yippee" } 79 | let(:false_setting) { "non-existent" } 80 | let(:setting_to_add) { "foo.bar.test" } 81 | let(:value_to_add) { Hocon::Impl::ConfigString.new(nil, "This is a test string") } 82 | let(:setting_to_remove) { "foo.bar" } 83 | 84 | context "parsing a .conf file" do 85 | let(:conf) { Hocon::ConfigFactory.parse_file(input_file) } 86 | include_examples "config_value_retrieval_single_value" 87 | include_examples "has_path_check" 88 | include_examples "add_value_to_config" 89 | include_examples "add_data_structures_to_config" 90 | include_examples "remove_value_from_config" 91 | end 92 | end 93 | 94 | context "example2" do 95 | let(:example) { EXAMPLE2 } 96 | let(:setting) { "jruby-puppet.jruby-pools" } 97 | let(:expected_setting) { "[{environment=production}]" } 98 | let(:false_setting) { "jruby-puppet-false" } 99 | let(:setting_to_add) { "top" } 100 | let(:value_to_add) { Hocon::Impl::ConfigInt.new(nil, 12345, "12345") } 101 | let(:setting_to_remove) { "jruby-puppet.master-conf-dir" } 102 | 103 | context "parsing a .conf file" do 104 | let(:conf) { Hocon::ConfigFactory.parse_file(input_file) } 105 | include_examples "config_value_retrieval_config_list" 106 | include_examples "has_path_check" 107 | include_examples "add_value_to_config" 108 | include_examples "add_data_structures_to_config" 109 | include_examples "remove_value_from_config" 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/hocon/impl/config_reference.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon' 4 | require_relative '../../hocon/impl' 5 | require_relative '../../hocon/impl/abstract_config_value' 6 | 7 | class Hocon::Impl::ConfigReference 8 | include Hocon::Impl::Unmergeable 9 | include Hocon::Impl::AbstractConfigValue 10 | 11 | # Require these lazily, to avoid circular dependencies 12 | require_relative '../../hocon/impl/resolve_source' 13 | require_relative '../../hocon/impl/resolve_result' 14 | 15 | 16 | NotPossibleToResolve = Hocon::Impl::AbstractConfigValue::NotPossibleToResolve 17 | UnresolvedSubstitutionError = Hocon::ConfigError::UnresolvedSubstitutionError 18 | 19 | attr_reader :expr, :prefix_length 20 | 21 | def initialize(origin, expr, prefix_length = 0) 22 | super(origin) 23 | @expr = expr 24 | @prefix_length = prefix_length 25 | end 26 | 27 | def unmerged_values 28 | [self] 29 | end 30 | 31 | # ConfigReference should be a firewall against NotPossibleToResolve going 32 | # further up the stack; it should convert everything to ConfigException. 33 | # This way it 's impossible for NotPossibleToResolve to "escape" since 34 | # any failure to resolve has to start with a ConfigReference. 35 | def resolve_substitutions(context, source) 36 | new_context = context.add_cycle_marker(self) 37 | begin 38 | result_with_path = source.lookup_subst(new_context, @expr, @prefix_length) 39 | new_context = result_with_path.result.context 40 | 41 | if result_with_path.result.value != nil 42 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 43 | Hocon::Impl::ConfigImpl.trace( 44 | "recursively resolving #{result_with_path} which was the resolution of #{expr} against #{source}", 45 | context.depth) 46 | end 47 | 48 | recursive_resolve_source = Hocon::Impl::ResolveSource.new( 49 | result_with_path.path_from_root.last, result_with_path.path_from_root) 50 | 51 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 52 | Hocon::Impl::ConfigImpl.trace("will recursively resolve against #{recursive_resolve_source}", context.depth) 53 | end 54 | 55 | result = new_context.resolve(result_with_path.result.value, 56 | recursive_resolve_source) 57 | v = result.value 58 | new_context = result.context 59 | else 60 | v = nil 61 | end 62 | rescue NotPossibleToResolve => e 63 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 64 | Hocon::Impl::ConfigImpl.trace( 65 | "not possible to resolve #{expr}, cycle involved: #{e.trace_string}", new_context.depth) 66 | end 67 | if @expr.optional 68 | v = nil 69 | else 70 | raise UnresolvedSubstitutionError.new( 71 | origin, 72 | "#{@expr} was part of a cycle of substitutions involving #{e.trace_string}", e) 73 | end 74 | end 75 | 76 | if v == nil && !@expr.optional 77 | if new_context.options.allow_unresolved 78 | ResolveResult.make(new_context.remove_cycle_marker(self), self) 79 | else 80 | raise UnresolvedSubstitutionError.new(origin, @expr.to_s) 81 | end 82 | else 83 | Hocon::Impl::ResolveResult.make(new_context.remove_cycle_marker(self), v) 84 | end 85 | 86 | end 87 | 88 | def value_type 89 | raise not_resolved 90 | end 91 | 92 | def unwrapped 93 | raise not_resolved 94 | end 95 | 96 | def new_copy(new_origin) 97 | Hocon::Impl::ConfigReference.new(new_origin, @expr, @prefix_length) 98 | end 99 | 100 | def ignores_fallbacks? 101 | false 102 | end 103 | 104 | def resolve_status 105 | Hocon::Impl::ResolveStatus::UNRESOLVED 106 | end 107 | 108 | def relativized(prefix) 109 | new_expr = @expr.change_path(@expr.path.prepend(prefix)) 110 | 111 | Hocon::Impl::ConfigReference.new(origin, new_expr, @prefix_length + prefix.length) 112 | end 113 | 114 | def can_equal(other) 115 | other.is_a? Hocon::Impl::ConfigReference 116 | end 117 | 118 | def ==(other) 119 | # note that "origin" is deliberately NOT part of equality 120 | if other.is_a? Hocon::Impl::ConfigReference 121 | can_equal(other) && @expr == other.expr 122 | end 123 | end 124 | 125 | def hash 126 | # note that "origin" is deliberately NOT part of equality 127 | @expr.hash 128 | end 129 | 130 | def render_value_to_sb(sb, indent, at_root, options) 131 | sb << @expr.to_s 132 | end 133 | 134 | def expression 135 | @expr 136 | end 137 | 138 | private 139 | 140 | def not_resolved 141 | error_message = "need to Config#resolve, see the API docs for Config#resolve; substitution not resolved: #{self}" 142 | Hocon::ConfigError::ConfigNotResolvedError.new(error_message, nil) 143 | end 144 | 145 | end 146 | -------------------------------------------------------------------------------- /spec/unit/cli/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'test_utils' 5 | 6 | 7 | describe Hocon::CLI do 8 | #################### 9 | # Argument Parsing 10 | #################### 11 | context 'argument parsing' do 12 | it 'should find all the flags and arguments' do 13 | args = %w(-i foo -o bar set some.path some_value --json) 14 | expected_options = { 15 | in_file: 'foo', 16 | out_file: 'bar', 17 | subcommand: 'set', 18 | path: 'some.path', 19 | new_value: 'some_value', 20 | json: true 21 | } 22 | expect(Hocon::CLI.parse_args(args)).to eq(expected_options) 23 | end 24 | 25 | it 'should set -i and -o to -f if given' do 26 | args = %w(-f foo set some.path some_value) 27 | expected_options = { 28 | file: 'foo', 29 | in_file: 'foo', 30 | out_file: 'foo', 31 | subcommand: 'set', 32 | path: 'some.path', 33 | new_value: 'some_value' 34 | } 35 | expect(Hocon::CLI.parse_args(args)).to eq(expected_options) 36 | end 37 | end 38 | 39 | context 'subcommands' do 40 | hocon_text = 41 | 'foo.bar { 42 | baz = 42 43 | array = [1, 2, 3] 44 | hash: {key: value} 45 | }' 46 | 47 | context 'do_get()' do 48 | it 'should get simple values' do 49 | options = {path: 'foo.bar.baz'} 50 | expect(Hocon::CLI.do_get(options, hocon_text)).to eq('42') 51 | end 52 | 53 | it 'should work with arrays' do 54 | options = {path: 'foo.bar.array'} 55 | expected = "[\n 1,\n 2,\n 3\n]" 56 | expect(Hocon::CLI.do_get(options, hocon_text)).to eq(expected) 57 | end 58 | 59 | it 'should work with hashes' do 60 | options = {path: 'foo.bar.hash'} 61 | expected = "key: value\n" 62 | expect(Hocon::CLI.do_get(options, hocon_text)).to eq(expected) 63 | end 64 | 65 | it 'should output json if specified' do 66 | options = {path: 'foo.bar.hash', json: true} 67 | 68 | # Note that this is valid json, while the test above is not 69 | expected = "{\n \"key\": \"value\"\n}\n" 70 | expect(Hocon::CLI.do_get(options, hocon_text)).to eq(expected) 71 | end 72 | 73 | it 'should throw a MissingPathError if the path does not exist' do 74 | options = {path: 'not.a.path'} 75 | expect {Hocon::CLI.do_get(options, hocon_text)} 76 | .to raise_error(Hocon::CLI::MissingPathError) 77 | end 78 | 79 | it 'should throw a MissingPathError if the path leads into an array' do 80 | options = {path: 'foo.array.1'} 81 | expect {Hocon::CLI.do_get(options, hocon_text)} 82 | .to raise_error(Hocon::CLI::MissingPathError) 83 | end 84 | 85 | it 'should throw a MissingPathError if the path leads into a string' do 86 | options = {path: 'foo.hash.key.value'} 87 | expect {Hocon::CLI.do_get(options, hocon_text)} 88 | .to raise_error(Hocon::CLI::MissingPathError) 89 | end 90 | end 91 | 92 | context 'do_set()' do 93 | it 'should overwrite values' do 94 | options = {path: 'foo.bar.baz', new_value: 'pi'} 95 | expected = hocon_text.sub(/42/, 'pi') 96 | expect(Hocon::CLI.do_set(options, hocon_text)).to eq(expected) 97 | end 98 | 99 | it 'should create new nested values' do 100 | options = {path: 'new.nested.path', new_value: 'hello'} 101 | expected = "new: {\n nested: {\n path: hello\n }\n}" 102 | # No config is supplied, so it will need to add new nested hashes 103 | expect(Hocon::CLI.do_set(options, '')).to eq(expected) 104 | end 105 | 106 | it 'should allow arrays to be set' do 107 | options = {path: 'my_array', new_value: '[1, 2, 3]'} 108 | expected = 'my_array: [1, 2, 3]' 109 | expect(Hocon::CLI.do_set(options, '')).to eq(expected) 110 | end 111 | 112 | it 'should allow arrays in strings to be set as strings' do 113 | options = {path: 'my_array', new_value: '"[1, 2, 3]"'} 114 | expected = 'my_array: "[1, 2, 3]"' 115 | expect(Hocon::CLI.do_set(options, '')).to eq(expected) 116 | end 117 | 118 | it 'should allow hashes to be set' do 119 | do_set_options = {path: 'my_hash', new_value: '{key: value}'} 120 | do_set_expected = 'my_hash: {key: value}' 121 | do_set_result = Hocon::CLI.do_set(do_set_options, '') 122 | expect(do_set_result).to eq(do_set_expected) 123 | 124 | # Make sure it can be parsed again and be seen as a real hash 125 | do_get_options = {path: 'my_hash.key'} 126 | do_get_expected = 'value' 127 | expect(Hocon::CLI.do_get(do_get_options, do_set_result)).to eq(do_get_expected) 128 | end 129 | 130 | it 'should allow hashes to be set as strings' do 131 | do_set_options = {path: 'my_hash', new_value: '"{key: value}"'} 132 | do_set_expected = 'my_hash: "{key: value}"' 133 | do_set_result = Hocon::CLI.do_set(do_set_options, '') 134 | expect(do_set_result).to eq(do_set_expected) 135 | 136 | # Make sure it can't be parsed again and be seen as a real hash 137 | do_get_options = {path: 'my_hash.key'} 138 | expect{Hocon::CLI.do_get(do_get_options, do_set_result)} 139 | .to raise_error(Hocon::CLI::MissingPathError) 140 | end 141 | end 142 | 143 | context 'do_unset()' do 144 | it 'should remove values' do 145 | options = {path: 'foo.bar.baz'} 146 | expected = hocon_text.sub(/baz = 42/, '') 147 | expect(Hocon::CLI.do_unset(options, hocon_text)).to eq(expected) 148 | end 149 | 150 | it 'should throw a MissingPathError if the path does not exist' do 151 | options = {path: 'fake.path'} 152 | expect{Hocon::CLI.do_unset(options, hocon_text)} 153 | .to raise_error(Hocon::CLI::MissingPathError) 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/unit/typesafe/config/token_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon' 5 | require 'test_utils' 6 | require 'pp' 7 | 8 | 9 | describe Hocon::Impl::Token do 10 | Tokens = Hocon::Impl::Tokens 11 | 12 | #################### 13 | # Equality 14 | #################### 15 | context "check token equality" do 16 | context "syntax tokens" do 17 | let(:first_object) { Tokens::START } 18 | let(:second_object) { Tokens::START } 19 | 20 | include_examples "object_equality" 21 | end 22 | 23 | context "integer tokens" do 24 | let(:first_object) { TestUtils.token_int(42) } 25 | let(:second_object) { TestUtils.token_int(42) } 26 | 27 | include_examples "object_equality" 28 | end 29 | 30 | context "truth tokens" do 31 | let(:first_object) { TestUtils.token_true } 32 | let(:second_object) { TestUtils.token_true } 33 | 34 | include_examples "object_equality" 35 | end 36 | 37 | context "int and double of the same value" do 38 | let(:first_object) { TestUtils.token_int(10) } 39 | let(:second_object) { TestUtils.token_double(10.0) } 40 | 41 | include_examples "object_equality" 42 | end 43 | 44 | context "double tokens" do 45 | let(:first_object) { TestUtils.token_int(3.14) } 46 | let(:second_object) { TestUtils.token_int(3.14) } 47 | 48 | include_examples "object_equality" 49 | end 50 | 51 | context "quoted string tokens" do 52 | let(:first_object) { TestUtils.token_string("foo") } 53 | let(:second_object) { TestUtils.token_string("foo") } 54 | 55 | include_examples "object_equality" 56 | end 57 | 58 | context "unquoted string tokens" do 59 | let(:first_object) { TestUtils.token_unquoted("foo") } 60 | let(:second_object) { TestUtils.token_unquoted("foo") } 61 | 62 | include_examples "object_equality" 63 | end 64 | 65 | context "key substitution tokens" do 66 | let(:first_object) { TestUtils.token_key_substitution("foo") } 67 | let(:second_object) { TestUtils.token_key_substitution("foo") } 68 | 69 | include_examples "object_equality" 70 | end 71 | 72 | context "null tokens" do 73 | let(:first_object) { TestUtils.token_null } 74 | let(:second_object) { TestUtils.token_null } 75 | 76 | include_examples "object_equality" 77 | end 78 | 79 | context "newline tokens" do 80 | let(:first_object) { TestUtils.token_line(10) } 81 | let(:second_object) { TestUtils.token_line(10) } 82 | 83 | include_examples "object_equality" 84 | end 85 | end 86 | 87 | 88 | #################### 89 | # Inequality 90 | #################### 91 | context "check token inequality" do 92 | context "syntax tokens" do 93 | let(:first_object) { Tokens::START } 94 | let(:second_object) { Tokens::OPEN_CURLY } 95 | 96 | include_examples "object_inequality" 97 | end 98 | 99 | context "integer tokens" do 100 | let(:first_object) { TestUtils.token_int(42) } 101 | let(:second_object) { TestUtils.token_int(43) } 102 | 103 | include_examples "object_inequality" 104 | end 105 | 106 | context "double tokens" do 107 | let(:first_object) { TestUtils.token_int(3.14) } 108 | let(:second_object) { TestUtils.token_int(4.14) } 109 | 110 | include_examples "object_inequality" 111 | end 112 | 113 | context "truth tokens" do 114 | let(:first_object) { TestUtils.token_true } 115 | let(:second_object) { TestUtils.token_false } 116 | 117 | include_examples "object_inequality" 118 | end 119 | 120 | context "quoted string tokens" do 121 | let(:first_object) { TestUtils.token_string("foo") } 122 | let(:second_object) { TestUtils.token_string("bar") } 123 | 124 | include_examples "object_inequality" 125 | end 126 | 127 | context "unquoted string tokens" do 128 | let(:first_object) { TestUtils.token_unquoted("foo") } 129 | let(:second_object) { TestUtils.token_unquoted("bar") } 130 | 131 | include_examples "object_inequality" 132 | end 133 | 134 | context "key substitution tokens" do 135 | let(:first_object) { TestUtils.token_key_substitution("foo") } 136 | let(:second_object) { TestUtils.token_key_substitution("bar") } 137 | 138 | include_examples "object_inequality" 139 | end 140 | 141 | context "newline tokens" do 142 | let(:first_object) { TestUtils.token_line(10) } 143 | let(:second_object) { TestUtils.token_line(11) } 144 | 145 | include_examples "object_inequality" 146 | end 147 | 148 | context "true and int tokens" do 149 | let(:first_object) { TestUtils.token_true } 150 | let(:second_object) { TestUtils.token_int(1) } 151 | 152 | include_examples "object_inequality" 153 | end 154 | 155 | context "string 'true' and true tokens" do 156 | let(:first_object) { TestUtils.token_true } 157 | let(:second_object) { TestUtils.token_string("true") } 158 | 159 | include_examples "object_inequality" 160 | end 161 | 162 | context "int and double of slightly different values" do 163 | let(:first_object) { TestUtils.token_int(10) } 164 | let(:second_object) { TestUtils.token_double(10.000001) } 165 | 166 | include_examples "object_inequality" 167 | end 168 | end 169 | 170 | context "Check that to_s doesn't throw exception" do 171 | it "shouldn't throw an exception" do 172 | # just be sure to_s doesn't throw an exception. It's for debugging 173 | # so its exact output doesn't matter a lot 174 | TestUtils.token_true.to_s 175 | TestUtils.token_false.to_s 176 | TestUtils.token_int(42).to_s 177 | TestUtils.token_double(3.14).to_s 178 | TestUtils.token_null.to_s 179 | TestUtils.token_unquoted("foo").to_s 180 | TestUtils.token_string("bar").to_s 181 | TestUtils.token_key_substitution("a").to_s 182 | TestUtils.token_line(10).to_s 183 | Tokens::START.to_s 184 | Tokens::EOF.to_s 185 | Tokens::COLON.to_s 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/hocon/config_object.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../hocon' 4 | require_relative '../hocon/config_value' 5 | 6 | # 7 | # Subtype of {@link ConfigValue} representing an object (AKA dictionary or map) 8 | # value, as in JSON's curly brace { "a" : 42 } syntax. 9 | # 10 | #

11 | # An object may also be viewed as a {@link Config} by calling 12 | # {@link ConfigObject#toConfig()}. 13 | # 14 | #

15 | # {@code ConfigObject} implements {@code java.util.Map} so 16 | # you can use it like a regular Java map. Or call {@link #unwrapped()} to 17 | # unwrap the map to a map with plain Java values rather than 18 | # {@code ConfigValue}. 19 | # 20 | #

21 | # Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable. 22 | # This makes it threadsafe and you never have to create "defensive copies." The 23 | # mutator methods from {@link java.util.Map} all throw 24 | # {@link java.lang.UnsupportedOperationException}. 25 | # 26 | #

27 | # The {@link ConfigValue#valueType} method on an object returns 28 | # {@link ConfigValueType#OBJECT}. 29 | # 30 | #

31 | # In most cases you want to use the {@link Config} interface rather than this 32 | # one. Call {@link #toConfig()} to convert a {@code ConfigObject} to a 33 | # {@code Config}. 34 | # 35 | #

36 | # The API for a {@code ConfigObject} is in terms of keys, while the API for a 37 | # {@link Config} is in terms of path expressions. Conceptually, 38 | # {@code ConfigObject} is a tree of maps from keys to values, while a 39 | # {@code Config} is a one-level map from paths to values. 40 | # 41 | #

42 | # Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert 43 | # between path expressions and individual path elements (keys). 44 | # 45 | #

46 | # A {@code ConfigObject} may contain null values, which will have 47 | # {@link ConfigValue#valueType()} equal to {@link ConfigValueType#NULL}. If 48 | # {@link ConfigObject#get(Object)} returns Java's null then the key was not 49 | # present in the parsed file (or wherever this value tree came from). If 50 | # {@code get("key")} returns a {@link ConfigValue} with type 51 | # {@code ConfigValueType#NULL} then the key was set to null explicitly in the 52 | # config file. 53 | # 54 | #

55 | # Do not implement interface {@code ConfigObject}; it should only be 56 | # implemented by the config library. Arbitrary implementations will not work 57 | # because the library internals assume a specific concrete implementation. 58 | # Also, this interface is likely to grow new methods over time, so third-party 59 | # implementations will break. 60 | # 61 | module Hocon::ConfigObject 62 | include Hocon::ConfigValue 63 | 64 | # 65 | # Converts this object to a {@link Config} instance, enabling you to use 66 | # path expressions to find values in the object. This is a constant-time 67 | # operation (it is not proportional to the size of the object). 68 | # 69 | # @return a {@link Config} with this object as its root 70 | # 71 | def to_config 72 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `to_config` (#{self.class})" 73 | end 74 | 75 | # 76 | # Recursively unwraps the object, returning a map from String to whatever 77 | # plain Java values are unwrapped from the object's values. 78 | # 79 | # @return a {@link java.util.Map} containing plain Java objects 80 | # 81 | def unwrapped 82 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `unwrapped` (#{self.class})" 83 | end 84 | 85 | def with_fallback(other) 86 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `with_fallback` (#{self.class})" 87 | end 88 | 89 | # 90 | # Gets a {@link ConfigValue} at the given key, or returns null if there is 91 | # no value. The returned {@link ConfigValue} may have 92 | # {@link ConfigValueType#NULL} or any other type, and the passed-in key 93 | # must be a key in this object (rather than a path expression). 94 | # 95 | # @param key 96 | # key to look up 97 | # 98 | # @return the value at the key or null if none 99 | # 100 | def get(key) 101 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `get` (#{self.class})" 102 | end 103 | 104 | # 105 | # Clone the object with only the given key (and its children) retained; all 106 | # sibling keys are removed. 107 | # 108 | # @param key 109 | # key to keep 110 | # @return a copy of the object minus all keys except the one specified 111 | # 112 | def with_only_key(key) 113 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `with_only_key` (#{self.class})" 114 | end 115 | 116 | # 117 | # Clone the object with the given key removed. 118 | # 119 | # @param key 120 | # key to remove 121 | # @return a copy of the object minus the specified key 122 | # 123 | def without_key(key) 124 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `without_key` (#{self.class})" 125 | end 126 | 127 | # 128 | # Returns a {@code ConfigObject} based on this one, but with the given key 129 | # set to the given value. Does not modify this instance (since it's 130 | # immutable). If the key already has a value, that value is replaced. To 131 | # remove a value, use {@link ConfigObject#withoutKey(String)}. 132 | # 133 | # @param key 134 | # key to add 135 | # @param value 136 | # value at the new key 137 | # @return the new instance with the new map entry 138 | # 139 | def with_value(key, value) 140 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `with_value` (#{self.class})" 141 | end 142 | 143 | def with_origin(origin) 144 | raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of ConfigObject should provide their own implementation of `with_origin` (#{self.class})" 145 | end 146 | 147 | end 148 | -------------------------------------------------------------------------------- /lib/hocon/impl/path.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/path_builder' 5 | require_relative '../../hocon/config_error' 6 | require 'stringio' 7 | 8 | class Hocon::Impl::Path 9 | 10 | ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 11 | ConfigImplUtil = Hocon::Impl::ConfigImplUtil 12 | 13 | def initialize(first, remainder) 14 | # first: String, remainder: Path 15 | 16 | @first = first 17 | @remainder = remainder 18 | end 19 | attr_reader :first, :remainder 20 | 21 | def self.from_string_list(elements) 22 | # This method was translated from the Path constructor in the 23 | # Java hocon library that has this signature: 24 | # Path(String... elements) 25 | # 26 | # It figures out what @first and @remainder should be, then 27 | # pass those to the ruby constructor 28 | if elements.length == 0 29 | raise Hocon::ConfigError::ConfigBugOrBrokenError.new("empty path") 30 | end 31 | 32 | new_first = elements.first 33 | 34 | if elements.length > 1 35 | pb = Hocon::Impl::PathBuilder.new 36 | 37 | # Skip first element 38 | elements.drop(1).each do |element| 39 | pb.append_key(element) 40 | end 41 | 42 | new_remainder = pb.result 43 | else 44 | new_remainder = nil 45 | end 46 | 47 | self.new(new_first, new_remainder) 48 | end 49 | 50 | def self.from_path_list(path_list) 51 | # This method was translated from the Path constructors in the 52 | # Java hocon library that take in a list of Paths 53 | # 54 | # It just passes an iterator to self.from_path_iterator, which 55 | # will return a new Path object 56 | from_path_iterator(path_list.each) 57 | end 58 | 59 | def self.from_path_iterator(path_iterator) 60 | # This method was translated from the Path constructors in the 61 | # Java hocon library that takes in an iterator of Paths 62 | # 63 | # It figures out what @first and @remainder should be, then 64 | # pass those to the ruby constructor 65 | 66 | # Try to get first path from iterator 67 | # Ruby iterators have no .hasNext() method like java 68 | # So we try to catch the StopIteration exception 69 | begin 70 | first_path = path_iterator.next 71 | rescue StopIteration 72 | raise Hocon::ConfigError::ConfigBugOrBrokenError("empty path") 73 | end 74 | 75 | new_first = first_path.first 76 | 77 | pb = Hocon::Impl::PathBuilder.new 78 | 79 | unless first_path.remainder.nil? 80 | pb.append_path(first_path.remainder) 81 | end 82 | 83 | # Skip first path 84 | path_iterator.drop(1).each do |path| 85 | pb.append_path(path) 86 | end 87 | 88 | new_remainder = pb.result 89 | 90 | self.new(new_first, new_remainder) 91 | end 92 | 93 | def first 94 | @first 95 | end 96 | 97 | def remainder 98 | @remainder 99 | end 100 | 101 | def parent 102 | if remainder.nil? 103 | return nil 104 | end 105 | 106 | pb = Hocon::Impl::PathBuilder.new 107 | p = self 108 | while not p.remainder.nil? 109 | pb.append_key(p.first) 110 | p = p.remainder 111 | end 112 | pb.result 113 | end 114 | 115 | def last 116 | p = self 117 | while p.remainder != nil 118 | p = p.remainder 119 | end 120 | p.first 121 | end 122 | 123 | def prepend(to_prepend) 124 | pb = Hocon::Impl::PathBuilder.new 125 | 126 | pb.append_path(to_prepend) 127 | pb.append_path(self) 128 | 129 | pb.result 130 | end 131 | 132 | def length 133 | count = 1 134 | p = remainder 135 | while p != nil do 136 | count += 1 137 | p = p.remainder 138 | end 139 | count 140 | end 141 | 142 | def sub_path(first_index, last_index) 143 | if last_index < first_index 144 | raise ConfigBugOrBrokenError.new("bad call to sub_path") 145 | end 146 | from = sub_path_to_end(first_index) 147 | pb = Hocon::Impl::PathBuilder.new 148 | count = last_index - first_index 149 | while count > 0 do 150 | count -= 1 151 | pb.append_key(from.first) 152 | from = from.remainder 153 | if from.nil? 154 | raise ConfigBugOrBrokenError.new("sub_path last_index out of range #{last_index}") 155 | end 156 | end 157 | pb.result 158 | end 159 | 160 | # translated from `subPath(int removeFromFront)` upstream 161 | def sub_path_to_end(remove_from_front) 162 | count = remove_from_front 163 | p = self 164 | while (not p.nil?) && count > 0 do 165 | count -= 1 166 | p = p.remainder 167 | end 168 | p 169 | end 170 | 171 | def starts_with(other) 172 | my_remainder = self 173 | other_remainder = other 174 | if other_remainder.length <= my_remainder.length 175 | while ! other_remainder.nil? 176 | if ! (other_remainder.first == my_remainder.first) 177 | return false 178 | end 179 | my_remainder = my_remainder.remainder 180 | other_remainder = other_remainder.remainder 181 | end 182 | return true 183 | end 184 | false 185 | end 186 | 187 | def ==(other) 188 | if other.is_a? Hocon::Impl::Path 189 | that = other 190 | first == that.first && ConfigImplUtil.equals_handling_nil?(remainder, that.remainder) 191 | else 192 | false 193 | end 194 | end 195 | 196 | def hash 197 | remainder_hash = remainder.nil? ? 0 : remainder.hash 198 | 199 | 41 * (41 + first.hash) + remainder_hash 200 | end 201 | 202 | # this doesn't have a very precise meaning, just to reduce 203 | # noise from quotes in the rendered path for average cases 204 | def self.has_funky_chars?(s) 205 | length = s.length 206 | if length == 0 207 | return false 208 | end 209 | 210 | s.chars.each do |c| 211 | unless (c =~ /[[:alnum:]]/) || (c == '-') || (c == '_') 212 | return true 213 | end 214 | end 215 | 216 | false 217 | end 218 | 219 | def append_to_string_builder(sb) 220 | if self.class.has_funky_chars?(@first) || @first.empty? 221 | sb << ConfigImplUtil.render_json_string(@first) 222 | else 223 | sb << @first 224 | end 225 | 226 | unless @remainder.nil? 227 | sb << "." 228 | @remainder.append_to_string_builder(sb) 229 | end 230 | end 231 | 232 | def to_s 233 | sb = StringIO.new 234 | sb << "Path(" 235 | append_to_string_builder(sb) 236 | sb << ")" 237 | 238 | sb.string 239 | end 240 | 241 | def inspect 242 | to_s 243 | end 244 | 245 | # 246 | # toString() is a debugging-oriented version while this is an 247 | # error-message-oriented human-readable one. 248 | # 249 | def render 250 | sb = StringIO.new 251 | append_to_string_builder(sb) 252 | sb.string 253 | end 254 | 255 | def self.new_key(key) 256 | return self.new(key, nil) 257 | end 258 | 259 | def self.new_path(path) 260 | Hocon::Impl::PathParser.parse_path(path) 261 | end 262 | 263 | end 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-hocon 2 | ========== 3 | [![Gem Version](https://badge.fury.io/rb/hocon.svg)](https://badge.fury.io/rb/hocon) [![Build Status](https://travis-ci.org/puppetlabs/ruby-hocon.png?branch=master)](https://travis-ci.org/puppetlabs/ruby-hocon) 4 | 5 | This is a port of the [Typesafe Config](https://github.com/typesafehub/config) library to Ruby. 6 | 7 | The library provides Ruby support for the [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) configuration file format. 8 | 9 | 10 | At present, it supports parsing and modification of existing HOCON/JSON files via the `ConfigFactory` 11 | class and the `ConfigValueFactory` class, and rendering parsed config objects back to a String 12 | ([see examples below](#basic-usage)). It also supports the parsing and modification of HOCON/JSON files via 13 | `ConfigDocumentFactory`. 14 | 15 | **Note:** While the project is production ready, since not all features in the Typesafe library are supported, 16 | you may still run into some issues. If you find a problem, feel free to open a github issue. 17 | 18 | The implementation is intended to be as close to a line-for-line port as the two languages allow, 19 | in hopes of making it fairly easy to port over new changesets from the Java code base over time. 20 | 21 | Support 22 | ======= 23 | 24 | For best results, if you find an issue with this library, please open an issue on our [Jira issue tracker](https://tickets.puppetlabs.com/browse/HC). Issues filed there tend to be more visible to the current maintainers than issues on the Github issue tracker. 25 | 26 | 27 | Basic Usage 28 | =========== 29 | 30 | ```sh 31 | gem install hocon 32 | ``` 33 | 34 | To use the simple API, for reading config values: 35 | 36 | ```rb 37 | require 'hocon' 38 | 39 | conf = Hocon.load("myapp.conf") 40 | puts "Here's a setting: #{conf["foo"]["bar"]["baz"]}" 41 | ``` 42 | 43 | By default, the simple API will determine the configuration file syntax/format 44 | based on the filename extension of the file; `.conf` will be interpreted as HOCON, 45 | `.json` will be interpreted as strict JSON, and any other extension will cause an 46 | error to be raised since the syntax is unknown. If you'd like to use a different 47 | file extension, you manually specify the syntax, like this: 48 | 49 | ```rb 50 | require 'hocon' 51 | require 'hocon/config_syntax' 52 | 53 | conf = Hocon.load("myapp.blah", {:syntax => Hocon::ConfigSyntax::HOCON}) 54 | ``` 55 | 56 | Supported values for `:syntax` are: JSON, CONF, and HOCON. (CONF and HOCON are 57 | aliases, and both map to the underlying HOCON syntax.) 58 | 59 | To use the ConfigDocument API, if you need both read/write capability for 60 | modifying settings in a config file, or if you want to retain access to 61 | things like comments and line numbers: 62 | 63 | ```rb 64 | require 'hocon/parser/config_document_factory' 65 | require 'hocon/config_value_factory' 66 | 67 | # The below 4 variables will all be ConfigDocument instances 68 | doc = Hocon::Parser::ConfigDocumentFactory.parse_file("myapp.conf") 69 | doc2 = doc.set_value("a.b", "[1, 2, 3, 4, 5]") 70 | doc3 = doc.remove_value("a") 71 | doc4 = doc.set_config_value("a.b", Hocon::ConfigValueFactory.from_any_ref([1, 2, 3, 4, 5])) 72 | 73 | doc_has_value = doc.has_value?("a") # returns boolean 74 | orig_doc_text = doc.render # returns string 75 | ``` 76 | 77 | Note that a `ConfigDocument` is used primarily for simple configuration manipulation while preserving 78 | whitespace and comments. As such, it is not powerful as the regular `Config` API, and will not resolve 79 | substitutions. 80 | 81 | CLI Tool 82 | ======== 83 | The `hocon` gem comes bundles with a `hocon` command line tool which can be used to get and set values from hocon files 84 | 85 | ``` 86 | Usage: hocon [options] {get,set,unset} PATH [VALUE] 87 | 88 | Example usages: 89 | hocon -i settings.conf -o new_settings.conf set some.nested.value 42 90 | hocon -f settings.conf set some.nested.value 42 91 | cat settings.conf | hocon get some.nested.value 92 | 93 | Subcommands: 94 | get PATH - Returns the value at the given path 95 | set PATH VALUE - Sets or adds the given value at the given path 96 | unset PATH - Removes the value at the given path 97 | 98 | Options: 99 | -i, --in-file HOCON_FILE HOCON file to read/modify. If omitted, STDIN assumed 100 | -o, --out-file HOCON_FILE File to be written to. If omitted, STDOUT assumed 101 | -f, --file HOCON_FILE File to read/write to. Equivalent to setting -i/-o to the same file 102 | -j, --json Output values from the 'get' subcommand in json format 103 | -h, --help Show this message 104 | -v, --version Show version 105 | ``` 106 | 107 | CLI Examples 108 | -------- 109 | ### Basic Usage 110 | ``` 111 | $ cat settings.conf 112 | { 113 | foo: bar 114 | } 115 | 116 | $ hocon -i settings.conf get foo 117 | bar 118 | 119 | $ hocon -i settings.conf set foo baz 120 | 121 | $ cat settings.conf 122 | { 123 | foo: baz 124 | } 125 | 126 | # Write to a different file 127 | $ hocon -i settings.conf -o new_settings.conf set some.nested.value 42 128 | $ cat new_settings.conf 129 | { 130 | foo: bar 131 | some: { 132 | nested: { 133 | value: 42 134 | 135 | } 136 | } 137 | } 138 | 139 | # Write back to the same file 140 | $ hocon -f settings.conf set some.nested.value 42 141 | $ cat settings.conf 142 | { 143 | foo: bar 144 | some: { 145 | nested: { 146 | value: 42 147 | 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | ### Complex Values 154 | If you give `set` a properly formatted hocon dictionary or array, it will try to accept it 155 | 156 | ``` 157 | $ hocon -i settings.conf set foo "{one: [1, 2, 3], two: {hello: world}}" 158 | { 159 | foo: {one: [1, 2, 3], two: {hello: world}} 160 | } 161 | ``` 162 | 163 | ### Chaining 164 | If `--in-file` or `--out-file` aren't specified, STDIN and STDOUT are used for the missing options. Therefore it's possible to chain `hocon` calls 165 | 166 | ``` 167 | $ cat settings.conf 168 | { 169 | foo: bar 170 | } 171 | 172 | $ cat settings.conf | hocon set foo 42 | hocon set one.two three 173 | { 174 | foo: 42 175 | one: { 176 | two: three 177 | } 178 | } 179 | ``` 180 | 181 | ### JSON Output 182 | Calls to the `get` subcommand will return the data in HOCON format by default, but setting the `-j/--json` flag will cause it to return a valid JSON object 183 | 184 | ``` 185 | $ cat settings.conf 186 | foo: { 187 | bar: { 188 | baz: 42 189 | } 190 | } 191 | 192 | $ hocon -i settings.conf get foo --json 193 | { 194 | "bar": { 195 | "baz": 42 196 | } 197 | } 198 | ``` 199 | 200 | Testing 201 | ======= 202 | 203 | ```sh 204 | bundle install --path .bundle 205 | bundle exec rspec spec 206 | ``` 207 | 208 | Unsupported Features 209 | ==================== 210 | 211 | This supports many of the same things as the Java library, but there are some notable exceptions. 212 | Unsupported features include: 213 | 214 | * Non file includes 215 | * Loading resources from the class path or URLs 216 | * Properties files 217 | * Parsing anything other than files and strings 218 | * Duration and size settings 219 | * Java system properties 220 | 221 | -------------------------------------------------------------------------------- /lib/hocon/impl/abstract_config_object.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon/impl' 4 | require_relative '../../hocon/impl/abstract_config_value' 5 | require_relative '../../hocon/impl/simple_config' 6 | require_relative '../../hocon/config_object' 7 | require_relative '../../hocon/config_value_type' 8 | require_relative '../../hocon/impl/resolve_status' 9 | require_relative '../../hocon/impl/simple_config_origin' 10 | require_relative '../../hocon/config_error' 11 | require_relative '../../hocon/impl/config_impl' 12 | require_relative '../../hocon/impl/unsupported_operation_error' 13 | require_relative '../../hocon/impl/container' 14 | 15 | module Hocon::Impl::AbstractConfigObject 16 | include Hocon::ConfigObject 17 | include Hocon::Impl::Container 18 | include Hocon::Impl::AbstractConfigValue 19 | 20 | ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 21 | ConfigNotResolvedError = Hocon::ConfigError::ConfigNotResolvedError 22 | 23 | def initialize(origin) 24 | super(origin) 25 | @config = Hocon::Impl::SimpleConfig.new(self) 26 | end 27 | 28 | def to_config 29 | @config 30 | end 31 | 32 | def to_fallback_value 33 | self 34 | end 35 | 36 | def with_only_key(key) 37 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `with_only_key`" 38 | end 39 | 40 | def without_key(key) 41 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `without_key`" 42 | end 43 | 44 | def with_value(key, value) 45 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `with_value`" 46 | end 47 | 48 | def with_only_path_or_nil(path) 49 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `with_only_path_or_nil`" 50 | end 51 | 52 | def with_only_path(path) 53 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `with_only_path`" 54 | end 55 | 56 | def without_path(path) 57 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `without_path`" 58 | end 59 | 60 | def with_path_value(path, value) 61 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `with_path_value`" 62 | end 63 | 64 | # This looks up the key with no transformation or type conversion of any 65 | # kind, and returns null if the key is not present. The object must be 66 | # resolved along the nodes needed to get the key or 67 | # ConfigNotResolvedError will be thrown. 68 | # 69 | # @param key 70 | # @return the unmodified raw value or null 71 | def peek_assuming_resolved(key, original_path) 72 | begin 73 | attempt_peek_with_partial_resolve(key) 74 | rescue ConfigNotResolvedError => e 75 | raise Hocon::Impl::ConfigImpl.improve_not_resolved(original_path, e) 76 | end 77 | end 78 | 79 | # Look up the key on an only-partially-resolved object, with no 80 | # transformation or type conversion of any kind; if 'this' is not resolved 81 | # then try to look up the key anyway if possible. 82 | # 83 | # @param key 84 | # key to look up 85 | # @return the value of the key, or null if known not to exist 86 | # @throws ConfigNotResolvedError 87 | # if can't figure out key's value (or existence) without more 88 | # resolving 89 | def attempt_peek_with_partial_resolve(key) 90 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `attempt_peek_with_partial_resolve`" 91 | end 92 | 93 | # Looks up the path with no transformation or type conversion. Returns null 94 | # if the path is not found; throws ConfigException.NotResolved if we need 95 | # to go through an unresolved node to look up the path. 96 | def peek_path(path) 97 | peek_path_from_obj(self, path) 98 | end 99 | 100 | def peek_path_from_obj(obj, path) 101 | begin 102 | # we'll fail if anything along the path can't be looked at without resolving 103 | path_next = path.remainder 104 | v = obj.attempt_peek_with_partial_resolve(path.first) 105 | 106 | if path_next.nil? 107 | v 108 | else 109 | if v.is_a?(Hocon::Impl::AbstractConfigObject) 110 | peek_path_from_obj(v, path_next) 111 | else 112 | nil 113 | end 114 | end 115 | rescue ConfigNotResolvedError => e 116 | raise Hocon::Impl::ConfigImpl.improve_not_resolved(path, e) 117 | end 118 | end 119 | 120 | def value_type 121 | Hocon::ConfigValueType::OBJECT 122 | end 123 | 124 | def new_copy_with_status(status, origin) 125 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `new_copy_with_status`" 126 | end 127 | 128 | def new_copy(origin) 129 | new_copy_with_status(resolve_status, origin) 130 | end 131 | 132 | def construct_delayed_merge(origin, stack) 133 | Hocon::Impl::ConfigDelayedMergeObject.new(origin, stack) 134 | end 135 | 136 | def merged_with_object(fallback) 137 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `merged_with_object`" 138 | end 139 | 140 | def with_fallback(mergeable) 141 | super(mergeable) 142 | end 143 | 144 | def self.merge_origins(stack) 145 | if stack.empty? 146 | raise ConfigBugOrBrokenError, "can't merge origins on empty list" 147 | end 148 | origins = [] 149 | first_origin = nil 150 | num_merged = 0 151 | stack.each do |v| 152 | if first_origin.nil? 153 | first_origin = v.origin 154 | end 155 | 156 | if (v.is_a?(Hocon::Impl::AbstractConfigObject)) && 157 | (v.resolve_status == Hocon::Impl::ResolveStatus::RESOLVED) && 158 | v.empty? 159 | # don't include empty files or the .empty() 160 | # config in the description, since they are 161 | # likely to be "implementation details" 162 | else 163 | origins.push(v.origin) 164 | num_merged += 1 165 | end 166 | end 167 | 168 | if num_merged == 0 169 | # the configs were all empty, so just use the first one 170 | origins.push(first_origin) 171 | end 172 | 173 | Hocon::Impl::SimpleConfigOrigin.merge_origins(origins) 174 | end 175 | 176 | def resolve_substitutions(context, source) 177 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `resolve_substituions`" 178 | end 179 | 180 | def relativized(path) 181 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `relativized`" 182 | end 183 | 184 | def [](key) 185 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `[]`" 186 | end 187 | 188 | def render_value_to_sb(sb, indent, at_root, options) 189 | raise ConfigBugOrBrokenError, "subclasses of AbstractConfigObject should override `render_value_to_sb`" 190 | end 191 | 192 | def we_are_immutable(method) 193 | Hocon::Impl::UnsupportedOperationError.new("ConfigObject is immutable, you can't call Map.#{method}") 194 | end 195 | 196 | def clear 197 | raise we_are_immutable("clear") 198 | end 199 | 200 | def []=(key, value) 201 | raise we_are_immutable("[]=") 202 | end 203 | 204 | def putAll(map) 205 | raise we_are_immutable("putAll") 206 | end 207 | 208 | def remove(key) 209 | raise we_are_immutable("remove") 210 | end 211 | 212 | def delete(key) 213 | raise we_are_immutable("delete") 214 | end 215 | 216 | def with_origin(origin) 217 | super(origin) 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/hocon/impl/simple_includer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'stringio' 4 | require_relative '../../hocon/impl' 5 | require_relative '../../hocon/impl/full_includer' 6 | require_relative '../../hocon/impl/url' 7 | require_relative '../../hocon/impl/config_impl' 8 | require_relative '../../hocon/config_error' 9 | require_relative '../../hocon/config_syntax' 10 | require_relative '../../hocon/impl/simple_config_object' 11 | require_relative '../../hocon/impl/simple_config_origin' 12 | require_relative '../../hocon/config_includer_file' 13 | require_relative '../../hocon/config_factory' 14 | require_relative '../../hocon/impl/parseable' 15 | 16 | class Hocon::Impl::SimpleIncluder < Hocon::Impl::FullIncluder 17 | 18 | ConfigBugorBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 19 | ConfigIOError = Hocon::ConfigError::ConfigIOError 20 | SimpleConfigObject = Hocon::Impl::SimpleConfigObject 21 | SimpleConfigOrigin = Hocon::Impl::SimpleConfigOrigin 22 | 23 | 24 | def initialize(fallback) 25 | @fallback = fallback 26 | end 27 | 28 | # ConfigIncludeContext does this for us on its options 29 | def self.clear_for_include(options) 30 | # the class loader and includer are inherited, but not this other stuff 31 | options.set_syntax(nil).set_origin_description(nil).set_allow_missing(true) 32 | end 33 | 34 | 35 | # this is the heuristic includer 36 | def include(context, name) 37 | obj = self.class.include_without_fallback(context, name) 38 | 39 | # now use the fallback includer if any and merge its result 40 | if ! (@fallback.nil?) 41 | obj.with_fallback(@fallback.include(context, name)) 42 | else 43 | obj 44 | end 45 | end 46 | 47 | # the heuristic includer in static form 48 | def self.include_without_fallback(context, name) 49 | # the heuristic is valid URL then URL, else relative to including file; 50 | # relativeTo in a file falls back to classpath inside relativeTo(). 51 | 52 | url = nil 53 | begin 54 | url = Hocon::Impl::Url.new(name) 55 | rescue Hocon::Impl::Url::MalformedUrlError => e 56 | url = nil 57 | end 58 | 59 | if !(url.nil?) 60 | include_url_without_fallback(context, url) 61 | else 62 | source = RelativeNameSource.new(context) 63 | from_basename(source, name, context.parse_options) 64 | end 65 | end 66 | 67 | # NOTE: not porting `include_url` or `include_url_without_fallback` from upstream, 68 | # because we probably won't support URL includes for now. 69 | 70 | def include_file(context, file) 71 | obj = self.class.include_file_without_fallback(context, file) 72 | 73 | # now use the fallback includer if any and merge its result 74 | if (!@fallback.nil?) && @fallback.is_a?(Hocon::ConfigIncluderFile) 75 | obj.with_fallback(@fallback).include_file(context, file) 76 | else 77 | obj 78 | end 79 | end 80 | 81 | def self.include_file_without_fallback(context, file) 82 | Hocon::ConfigFactory.parse_file_any_syntax(file, context.parse_options).root 83 | end 84 | 85 | # NOTE: not porting `include_resources` or `include_resources_without_fallback` 86 | # for now because we're not going to support looking for things on the ruby 87 | # load path for now. 88 | 89 | def with_fallback(fallback) 90 | if self.equal?(fallback) 91 | raise ConfigBugOrBrokenError, "trying to create includer cycle" 92 | elsif @fallback.equal?(fallback) 93 | self 94 | elsif @fallback.nil? 95 | self.class.new(@fallback.with_fallback(fallback)) 96 | else 97 | self.class.new(fallback) 98 | end 99 | end 100 | 101 | 102 | class NameSource 103 | def name_to_parseable(name, parse_options) 104 | raise Hocon::ConfigError::ConfigBugOrBrokenError, 105 | "name_to_parseable must be implemented by subclass (#{self.class})" 106 | end 107 | end 108 | 109 | class RelativeNameSource < NameSource 110 | def initialize(context) 111 | @context = context 112 | end 113 | 114 | def name_to_parseable(name, options) 115 | p = @context.relative_to(name) 116 | if p.nil? 117 | # avoid returning nil 118 | Hocon::Impl::Parseable.new_not_found(name, "include was not found: '#{name}'", options) 119 | else 120 | p 121 | end 122 | end 123 | end 124 | 125 | # this function is a little tricky because there are three places we're 126 | # trying to use it; for 'include "basename"' in a .conf file, for 127 | # loading app.{conf,json,properties} from classpath, and for 128 | # loading app.{conf,json,properties} from the filesystem. 129 | def self.from_basename(source, name, options) 130 | obj = nil 131 | if name.end_with?(".conf") || name.end_with?(".json") || name.end_with?(".properties") 132 | p = source.name_to_parseable(name, options) 133 | 134 | obj = p.parse(p.options.set_allow_missing(options.allow_missing?)) 135 | else 136 | conf_handle = source.name_to_parseable(name + ".conf", options) 137 | json_handle = source.name_to_parseable(name + ".json", options) 138 | got_something = false 139 | fails = [] 140 | 141 | syntax = options.syntax 142 | 143 | obj = SimpleConfigObject.empty(SimpleConfigOrigin.new_simple(name)) 144 | if syntax.nil? || (syntax == Hocon::ConfigSyntax::CONF) 145 | begin 146 | obj = conf_handle.parse(conf_handle.options.set_allow_missing(false). 147 | set_syntax(Hocon::ConfigSyntax::CONF)) 148 | got_something = true 149 | rescue ConfigIOError => e 150 | fails << e 151 | end 152 | end 153 | 154 | if syntax.nil? || (syntax == Hocon::ConfigSyntax::JSON) 155 | begin 156 | parsed = json_handle.parse(json_handle.options.set_allow_missing(false). 157 | set_syntax(Hocon::ConfigSyntax::JSON)) 158 | obj = obj.with_fallback(parsed) 159 | got_something = true 160 | rescue ConfigIOError => e 161 | fails << e 162 | end 163 | end 164 | 165 | # NOTE: skipping the upstream block here that would attempt to parse 166 | # a java properties file. 167 | 168 | if (! options.allow_missing?) && (! got_something) 169 | if Hocon::Impl::ConfigImpl.trace_loads_enabled 170 | # the individual exceptions should have been logged already 171 | # with tracing enabled 172 | Hocon::Impl::ConfigImpl.trace("Did not find '#{name}'" + 173 | " with any extension (.conf, .json, .properties); " + 174 | "exceptions should have been logged above.") 175 | end 176 | 177 | if fails.empty? 178 | # this should not happen 179 | raise ConfigBugOrBrokenError, "should not be reached: nothing found but no exceptions thrown" 180 | else 181 | sb = StringIO.new 182 | fails.each do |t| 183 | sb << t 184 | sb << ", " 185 | end 186 | raise ConfigIOError.new(SimpleConfigOrigin.new_simple(name), sb.string, fails[0]) 187 | end 188 | elsif !got_something 189 | if Hocon::Impl::ConfigImpl.trace_loads_enabled 190 | Hocon::Impl::ConfigImpl.trace("Did not find '#{name}'" + 191 | " with any extension (.conf, .json, .properties); but '#{name}'" + 192 | " is allowed to be missing. Exceptions from load attempts should have been logged above.") 193 | end 194 | end 195 | end 196 | 197 | obj 198 | end 199 | 200 | class Proxy < Hocon::Impl::FullIncluder 201 | def initialize(delegate) 202 | @delegate = delegate 203 | end 204 | ## TODO: port remaining implementation when needed 205 | end 206 | 207 | def self.make_full(includer) 208 | if includer.is_a?(Hocon::Impl::FullIncluder) 209 | includer 210 | else 211 | Proxy.new(includer) 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /lib/hocon/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require_relative '../hocon' 3 | require_relative '../hocon/version' 4 | require_relative '../hocon/config_render_options' 5 | require_relative '../hocon/config_factory' 6 | require_relative '../hocon/config_value_factory' 7 | require_relative '../hocon/parser/config_document_factory' 8 | require_relative '../hocon/config_error' 9 | 10 | module Hocon::CLI 11 | # Aliases 12 | ConfigMissingError = Hocon::ConfigError::ConfigMissingError 13 | ConfigWrongTypeError = Hocon::ConfigError::ConfigWrongTypeError 14 | 15 | # List of valid subcommands 16 | SUBCOMMANDS = ['get', 'set', 'unset'] 17 | 18 | # For when a path can't be found in a hocon config 19 | class MissingPathError < StandardError 20 | end 21 | 22 | # Parses the command line flags and argument 23 | # Returns a options hash with values for each option and argument 24 | def self.parse_args(args) 25 | options = {} 26 | opt_parser = OptionParser.new do |opts| 27 | subcommands = SUBCOMMANDS.join(',') 28 | opts.banner = "Usage: hocon [options] {#{subcommands}} PATH [VALUE]\n\n" + 29 | "Example usages:\n" + 30 | " hocon -i settings.conf -o new_settings.conf set some.nested.value 42\n" + 31 | " hocon -f settings.conf set some.nested.value 42\n" + 32 | " cat settings.conf | hocon get some.nested.value\n\n" + 33 | "Subcommands:\n" + 34 | " get PATH - Returns the value at the given path\n" + 35 | " set PATH VALUE - Sets or adds the given value at the given path\n" + 36 | " unset PATH - Removes the value at the given path" 37 | 38 | opts.separator('') 39 | opts.separator('Options:') 40 | 41 | in_file_description = 'HOCON file to read/modify. If omitted, STDIN assumed' 42 | opts.on('-i', '--in-file HOCON_FILE', in_file_description) do |in_file| 43 | options[:in_file] = in_file 44 | end 45 | 46 | out_file_description = 'File to be written to. If omitted, STDOUT assumed' 47 | opts.on('-o', '--out-file HOCON_FILE', out_file_description) do |out_file| 48 | options[:out_file] = out_file 49 | end 50 | 51 | file_description = 'File to read/write to. Equivalent to setting -i/-o to the same file' 52 | opts.on('-f', '--file HOCON_FILE', file_description) do |file| 53 | options[:file] = file 54 | end 55 | 56 | json_description = "Output values from the 'get' subcommand in json format" 57 | opts.on('-j', '--json', json_description) do |json| 58 | options[:json] = json 59 | end 60 | 61 | opts.on_tail('-h', '--help', 'Show this message') do 62 | puts opts 63 | exit 64 | end 65 | 66 | opts.on_tail('-v', '--version', 'Show version') do 67 | puts Hocon::Version::STRING 68 | exit 69 | end 70 | end 71 | # parse! returns the argument list minus all the flags it found 72 | remaining_args = opt_parser.parse!(args) 73 | 74 | # Ensure -i and -o aren't used at the same time as -f 75 | if (options[:in_file] || options[:out_file]) && options[:file] 76 | exit_with_usage_and_error(opt_parser, "--file can't be used with --in-file or --out-file") 77 | end 78 | 79 | # If --file is used, set --in/out-file to the same file 80 | if options[:file] 81 | options[:in_file] = options[:file] 82 | options[:out_file] = options[:file] 83 | end 84 | 85 | no_subcommand_error(opt_parser) unless remaining_args.size > 0 86 | 87 | # Assume the first arg is the subcommand 88 | subcommand = remaining_args.shift 89 | options[:subcommand] = subcommand 90 | 91 | case subcommand 92 | when 'set' 93 | subcommand_arguments_error(subcommand, opt_parser) unless remaining_args.size >= 2 94 | options[:path] = remaining_args.shift 95 | options[:new_value] = remaining_args.shift 96 | 97 | when 'get', 'unset' 98 | subcommand_arguments_error(subcommand, opt_parser) unless remaining_args.size >= 1 99 | options[:path] = remaining_args.shift 100 | 101 | else 102 | invalid_subcommand_error(subcommand, opt_parser) 103 | end 104 | 105 | options 106 | end 107 | 108 | # Main entry point into the script 109 | # Calls the appropriate subcommand and handles errors raised from the subcommands 110 | def self.main(opts) 111 | hocon_text = get_hocon_file(opts[:in_file]) 112 | 113 | begin 114 | case opts[:subcommand] 115 | when 'get' 116 | puts do_get(opts, hocon_text) 117 | when 'set' 118 | print_or_write(do_set(opts, hocon_text), opts[:out_file]) 119 | when 'unset' 120 | print_or_write(do_unset(opts, hocon_text), opts[:out_file]) 121 | end 122 | 123 | rescue MissingPathError 124 | exit_with_error("Can't find the given path: '#{opts[:path]}'") 125 | end 126 | 127 | exit 128 | end 129 | 130 | # Entry point for the 'get' subcommand 131 | # Returns a string representation of the the value at the path given on the 132 | # command line 133 | def self.do_get(opts, hocon_text) 134 | config = Hocon::ConfigFactory.parse_string(hocon_text) 135 | unless config.has_path?(opts[:path]) 136 | raise MissingPathError.new 137 | end 138 | 139 | value = config.get_any_ref(opts[:path]) 140 | 141 | render_options = Hocon::ConfigRenderOptions.defaults 142 | # Otherwise weird comments show up in the output 143 | render_options.origin_comments = false 144 | # If json is false, the hocon format is used 145 | render_options.json = opts[:json] 146 | # Output colons between keys and values 147 | render_options.key_value_separator = :colon 148 | 149 | Hocon::ConfigValueFactory.from_any_ref(value).render(render_options) 150 | end 151 | 152 | # Entry point for the 'set' subcommand 153 | # Returns a string representation of the HOCON config after adding/replacing 154 | # the value at the given path with the given value 155 | def self.do_set(opts, hocon_text) 156 | config_doc = Hocon::Parser::ConfigDocumentFactory.parse_string(hocon_text) 157 | modified_config_doc = config_doc.set_value(opts[:path], opts[:new_value]) 158 | 159 | modified_config_doc.render 160 | end 161 | 162 | # Entry point for the 'unset' subcommand 163 | # Returns a string representation of the HOCON config after removing the 164 | # value at the given path 165 | def self.do_unset(opts, hocon_text) 166 | config_doc = Hocon::Parser::ConfigDocumentFactory.parse_string(hocon_text) 167 | unless config_doc.has_value?(opts[:path]) 168 | raise MissingPathError.new 169 | end 170 | 171 | modified_config_doc = config_doc.remove_value(opts[:path]) 172 | 173 | modified_config_doc.render 174 | end 175 | 176 | # If a file is provided, return it's contents. Otherwise read from STDIN 177 | def self.get_hocon_file(in_file) 178 | if in_file 179 | File.read(in_file) 180 | else 181 | STDIN.read 182 | end 183 | end 184 | 185 | # Print an error message and exit the program 186 | def self.exit_with_error(message) 187 | STDERR.puts "Error: #{message}" 188 | exit(1) 189 | end 190 | 191 | # Print an error message and usage, then exit the program 192 | def self.exit_with_usage_and_error(opt_parser, message) 193 | STDERR.puts opt_parser 194 | exit_with_error(message) 195 | end 196 | 197 | # Exits with an error saying there aren't enough arguments found for a given 198 | # subcommand. Prints the usage 199 | def self.subcommand_arguments_error(subcommand, opt_parser) 200 | error_message = "Too few arguments for '#{subcommand}' subcommand" 201 | exit_with_usage_and_error(opt_parser, error_message) 202 | end 203 | 204 | # Exits with an error for when no subcommand is supplied on the command line. 205 | # Prints the usage 206 | def self.no_subcommand_error(opt_parser) 207 | error_message = "Must specify subcommand from [#{SUBCOMMANDS.join(', ')}]" 208 | exit_with_usage_and_error(opt_parser, error_message) 209 | end 210 | 211 | # Exits with an error for when a subcommand doesn't exist. Prints the usage 212 | def self.invalid_subcommand_error(subcommand, opt_parser) 213 | error_message = "Invalid subcommand '#{subcommand}', must be one of [#{SUBCOMMANDS.join(', ')}]" 214 | exit_with_usage_and_error(opt_parser, error_message) 215 | end 216 | 217 | # If out_file is not nil, write to that file. Otherwise print to STDOUT 218 | def self.print_or_write(string, out_file) 219 | if out_file 220 | File.open(out_file, 'w') { |file| file.write(string) } 221 | else 222 | puts string 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/unit/typesafe/config/path_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'hocon' 5 | require 'test_utils' 6 | 7 | 8 | describe Hocon::Impl::Path do 9 | Path = Hocon::Impl::Path 10 | 11 | #################### 12 | # Path Equality 13 | #################### 14 | context "Check path equality" do 15 | # note: foo.bar is a single key here 16 | let(:key_a) { Path.new_key("foo.bar") } 17 | let(:same_as_key_a) { Path.new_key("foo.bar") } 18 | let(:different_key) { Path.new_key("hello") } 19 | 20 | # Here foo.bar is two elements 21 | let(:two_elements) { Path.new_path("foo.bar") } 22 | let(:same_as_two_elements) { Path.new_path("foo.bar") } 23 | 24 | context "key_a equals a path of the same name" do 25 | let(:first_object) { key_a } 26 | let(:second_object) { TestUtils.path("foo.bar") } 27 | include_examples "object_equality" 28 | end 29 | 30 | context "two_elements equals a path with those two elements" do 31 | let(:first_object) { two_elements} 32 | let(:second_object) { TestUtils.path("foo", "bar") } 33 | include_examples "object_equality" 34 | end 35 | 36 | context "key_a equals key_a" do 37 | let(:first_object) { key_a } 38 | let(:second_object) { key_a } 39 | include_examples "object_equality" 40 | end 41 | 42 | context "key_a equals same_as_key_a" do 43 | let(:first_object) { key_a } 44 | let(:second_object) { same_as_key_a } 45 | include_examples "object_equality" 46 | end 47 | 48 | context "key_a not equal to different_key" do 49 | let(:first_object) { key_a } 50 | let(:second_object) { different_key } 51 | include_examples "object_inequality" 52 | end 53 | 54 | context "key_a not equal to the two_elements path" do 55 | let(:first_object) { key_a } 56 | let(:second_object) { two_elements } 57 | include_examples "object_inequality" 58 | end 59 | 60 | context "two_elements path equals same_as_two_elements path" do 61 | let(:first_object) { two_elements} 62 | let(:second_object) { same_as_two_elements } 63 | include_examples "object_equality" 64 | end 65 | end 66 | 67 | #################### 68 | # Testing to_s 69 | #################### 70 | context "testing to_s" do 71 | it "should find to_s returning the correct strings" do 72 | expect("Path(foo)").to eq(TestUtils.path("foo").to_s) 73 | expect("Path(foo.bar)").to eq(TestUtils.path("foo", "bar").to_s) 74 | expect('Path(foo."bar*")').to eq(TestUtils.path("foo", "bar*").to_s) 75 | expect('Path("foo.bar")').to eq(TestUtils.path("foo.bar").to_s) 76 | end 77 | end 78 | 79 | #################### 80 | # Render 81 | #################### 82 | context "testing .render" do 83 | context "rendering simple one element case" do 84 | let(:expected) { "foo" } 85 | let(:path) { TestUtils.path("foo") } 86 | include_examples "path_render_test" 87 | end 88 | 89 | context "rendering simple two element case" do 90 | let(:expected) { "foo.bar" } 91 | let(:path) { TestUtils.path("foo", "bar") } 92 | include_examples "path_render_test" 93 | end 94 | 95 | context "rendering non safe char in an element" do 96 | let(:expected) { 'foo."bar*"' } 97 | let(:path) { TestUtils.path("foo", "bar*") } 98 | include_examples "path_render_test" 99 | end 100 | 101 | context "rendering period in an element" do 102 | let(:expected) { '"foo.bar"' } 103 | let(:path) { TestUtils.path("foo.bar") } 104 | include_examples "path_render_test" 105 | end 106 | 107 | context "rendering hyphen in element" do 108 | let(:expected) { "foo-bar" } 109 | let(:path) { TestUtils.path("foo-bar") } 110 | include_examples "path_render_test" 111 | end 112 | 113 | context "rendering hyphen in element" do 114 | let(:expected) { "foo_bar" } 115 | let(:path) { TestUtils.path("foo_bar") } 116 | include_examples "path_render_test" 117 | end 118 | 119 | context "rendering element starting with a hyphen" do 120 | let(:expected) { "-foo" } 121 | let(:path) { TestUtils.path("-foo") } 122 | include_examples "path_render_test" 123 | end 124 | 125 | context "rendering element starting with a number" do 126 | let(:expected) { "10foo" } 127 | let(:path) { TestUtils.path("10foo") } 128 | include_examples "path_render_test" 129 | end 130 | 131 | context "rendering empty elements" do 132 | let(:expected) { '"".""' } 133 | let(:path) { TestUtils.path("", "") } 134 | include_examples "path_render_test" 135 | end 136 | 137 | context "rendering element with internal space" do 138 | let(:expected) { '"foo bar"' } 139 | let(:path) { TestUtils.path("foo bar") } 140 | include_examples "path_render_test" 141 | end 142 | 143 | context "rendering leading and trailing spaces" do 144 | let(:expected) { '" foo "' } 145 | let(:path) { TestUtils.path(" foo ") } 146 | include_examples "path_render_test" 147 | end 148 | 149 | context "rendering trailing space only" do 150 | let(:expected) { '"foo "' } 151 | let(:path) { TestUtils.path("foo ") } 152 | include_examples "path_render_test" 153 | end 154 | 155 | context "rendering number with decimal point" do 156 | let(:expected) { "1.2" } 157 | let(:path) { TestUtils.path("1", "2") } 158 | include_examples "path_render_test" 159 | end 160 | 161 | context "rendering number with multiple decimal points" do 162 | let(:expected) { "1.2.3.4" } 163 | let(:path) { TestUtils.path("1", "2", "3", "4") } 164 | include_examples "path_render_test" 165 | end 166 | end 167 | 168 | context "test that paths made from a list of Path objects equal paths made from a list of strings" do 169 | it "should find a path made from a list of one path equal to a path from one string" do 170 | path_from_path_list = Path.from_path_list([TestUtils.path("foo")]) 171 | expected_path = TestUtils.path("foo") 172 | 173 | expect(path_from_path_list).to eq(expected_path) 174 | end 175 | 176 | it "should find a path made from a list of multiple paths equal to that list of strings" do 177 | path_from_path_list = Path.from_path_list([TestUtils.path("foo", "bar"), 178 | TestUtils.path("baz", "boo")]) 179 | expected_path = TestUtils.path("foo", "bar", "baz", "boo") 180 | 181 | expect(path_from_path_list).to eq(expected_path) 182 | end 183 | end 184 | 185 | context "prepending paths" do 186 | it "should find prepending a single path works" do 187 | prepended_path = TestUtils.path("bar").prepend(TestUtils.path("foo")) 188 | expected_path = TestUtils.path("foo", "bar") 189 | 190 | expect(prepended_path).to eq(expected_path) 191 | end 192 | 193 | it "should find prepending multiple paths works" do 194 | prepended_path = TestUtils.path("c", "d").prepend(TestUtils.path("a", "b")) 195 | expected_path = TestUtils.path("a", "b", "c", "d") 196 | 197 | expect(prepended_path).to eq(expected_path) 198 | end 199 | end 200 | 201 | context "path length" do 202 | it "should find length of single part path to be 1" do 203 | path = TestUtils.path("food") 204 | expect(path.length).to eq(1) 205 | end 206 | 207 | it "should find length of two part path to be 2" do 208 | path = TestUtils.path("foo", "bar") 209 | expect(path.length).to eq(2) 210 | 211 | end 212 | end 213 | 214 | context "parent paths" do 215 | it "should find parent of single level path to be nil" do 216 | path = TestUtils.path("a") 217 | 218 | expect(path.parent).to be_nil 219 | end 220 | 221 | it "should find parent of a.b to be a" do 222 | path = TestUtils.path("a", "b") 223 | parent = TestUtils.path("a") 224 | 225 | expect(path.parent).to eq(parent) 226 | end 227 | 228 | it "should find parent of a.b.c to be a.b" do 229 | path = TestUtils.path("a", "b", "c") 230 | parent = TestUtils.path("a", "b") 231 | 232 | expect(path.parent).to eq(parent) 233 | end 234 | end 235 | 236 | context "path last method" do 237 | it "should find last of single level path to be itself" do 238 | path = TestUtils.path("a") 239 | 240 | expect(path.last).to eq("a") 241 | end 242 | 243 | it "should find last of a.b to be b" do 244 | path = TestUtils.path("a", "b") 245 | 246 | expect(path.last).to eq("b") 247 | end 248 | end 249 | 250 | context "invalid paths" do 251 | it "should catch exception from empty path" do 252 | bad_path = "" 253 | expect { Path.new_path(bad_path) }.to raise_error(Hocon::ConfigError::ConfigBadPathError) 254 | end 255 | 256 | it "should catch exception from path '..'" do 257 | bad_path = ".." 258 | expect { Path.new_path(bad_path) }.to raise_error(Hocon::ConfigError::ConfigBadPathError) 259 | end 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /lib/hocon/impl/resolve_context.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../hocon' 4 | require_relative '../../hocon/config_error' 5 | require_relative '../../hocon/impl/resolve_source' 6 | require_relative '../../hocon/impl/resolve_memos' 7 | require_relative '../../hocon/impl/memo_key' 8 | require_relative '../../hocon/impl/abstract_config_value' 9 | require_relative '../../hocon/impl/config_impl' 10 | 11 | class Hocon::Impl::ResolveContext 12 | 13 | ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError 14 | NotPossibleToResolve = Hocon::Impl::AbstractConfigValue::NotPossibleToResolve 15 | 16 | attr_reader :restrict_to_child 17 | 18 | def initialize(memos, options, restrict_to_child, resolve_stack, cycle_markers) 19 | @memos = memos 20 | @options = options 21 | @restrict_to_child = restrict_to_child 22 | @resolve_stack = resolve_stack 23 | @cycle_markers = cycle_markers 24 | end 25 | 26 | def self.new_cycle_markers 27 | # This looks crazy, but wtf else should we do with 28 | # return Collections.newSetFromMap(new IdentityHashMap()); 29 | Set.new 30 | end 31 | 32 | def add_cycle_marker(value) 33 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 34 | Hocon::Impl::ConfigImpl.trace("++ Cycle marker #{value}@#{value.hash}", 35 | depth) 36 | end 37 | if @cycle_markers.include?(value) 38 | raise ConfigBugOrBrokenError.new("Added cycle marker twice " + value) 39 | end 40 | copy = self.class.new_cycle_markers 41 | copy.merge(@cycle_markers) 42 | copy.add(value) 43 | self.class.new(@memos, @options, @restrict_to_child, @resolve_stack, copy) 44 | end 45 | 46 | def remove_cycle_marker(value) 47 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 48 | Hocon::Impl::ConfigImpl.trace("-- Cycle marker #{value}@#{value.hash}", 49 | depth) 50 | end 51 | 52 | copy = self.class.new_cycle_markers 53 | copy.merge(@cycle_markers) 54 | copy.delete(value) 55 | self.class.new(@memos, @options, @restrict_to_child, @resolve_stack, copy) 56 | end 57 | 58 | def memoize(key, value) 59 | changed = @memos.put(key, value) 60 | self.class.new(changed, @options, @restrict_to_child, @resolve_stack, @cycle_markers) 61 | end 62 | 63 | def options 64 | @options 65 | end 66 | 67 | def is_restricted_to_child 68 | @restrict_to_child != nil 69 | end 70 | 71 | def restrict(restrict_to) 72 | if restrict_to.equal?(@restrict_to_child) 73 | self 74 | else 75 | Hocon::Impl::ResolveContext.new(@memos, @options, restrict_to, @resolve_stack, @cycle_markers) 76 | end 77 | end 78 | 79 | def unrestricted 80 | restrict(nil) 81 | end 82 | 83 | def resolve(original, source) 84 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 85 | Hocon::Impl::ConfigImpl.trace( 86 | "resolving #{original} restrict_to_child=#{@restrict_to_child} in #{source}", 87 | depth) 88 | end 89 | push_trace(original).real_resolve(original, source).pop_trace 90 | end 91 | 92 | def real_resolve(original, source) 93 | # a fully-resolved (no restrict_to_child) object can satisfy a 94 | # request for a restricted object, so always check that first. 95 | full_key = Hocon::Impl::MemoKey.new(original, nil) 96 | restricted_key = nil 97 | 98 | cached = @memos.get(full_key) 99 | 100 | # but if there was no fully-resolved object cached, we'll only 101 | # compute the restrictToChild object so use a more limited 102 | # memo key 103 | if cached == nil && is_restricted_to_child 104 | restricted_key = Hocon::Impl::MemoKey.new(original, @restrict_to_child) 105 | cached = @memos.get(restricted_key) 106 | end 107 | 108 | if cached != nil 109 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 110 | Hocon::Impl::ConfigImpl.trace( 111 | "using cached resolution #{cached} for #{original} restrict_to_child #{@restrict_to_child}", 112 | depth) 113 | end 114 | Hocon::Impl::ResolveResult.make(self, cached) 115 | else 116 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 117 | Hocon::Impl::ConfigImpl.trace( 118 | "not found in cache, resolving #{original}@#{original.hash}", 119 | depth) 120 | end 121 | 122 | if @cycle_markers.include?(original) 123 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 124 | Hocon::Impl::ConfigImpl.trace( 125 | "Cycle detected, can't resolve; #{original}@#{original.hash}", 126 | depth) 127 | end 128 | raise NotPossibleToResolve.new(self) 129 | end 130 | 131 | result = original.resolve_substitutions(self, source) 132 | resolved = result.value 133 | 134 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 135 | Hocon::Impl::ConfigImpl.trace( 136 | "resolved to #{resolved}@#{resolved.hash} from #{original}@#{resolved.hash}", 137 | depth) 138 | end 139 | 140 | with_memo = result.context 141 | 142 | if resolved == nil || resolved.resolve_status == Hocon::Impl::ResolveStatus::RESOLVED 143 | # if the resolved object is fully resolved by resolving 144 | # only the restrictToChildOrNull, then it can be cached 145 | # under fullKey since the child we were restricted to 146 | # turned out to be the only unresolved thing. 147 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 148 | Hocon::Impl::ConfigImpl.trace( 149 | "caching #{full_key} result #{resolved}", 150 | depth) 151 | end 152 | 153 | with_memo = with_memo.memoize(full_key, resolved) 154 | else 155 | # if we have an unresolved object then either we did a 156 | # partial resolve restricted to a certain child, or we are 157 | # allowing incomplete resolution, or it's a bug. 158 | if is_restricted_to_child 159 | if restricted_key == nil 160 | raise ConfigBugOrBrokenError.new("restricted_key should not be null here") 161 | end 162 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 163 | Hocon::Impl::ConfigImpl.trace( 164 | "caching #{restricted_key} result #{resolved}", 165 | depth) 166 | end 167 | 168 | with_memo = with_memo.memoize(restricted_key, resolved) 169 | elsif @options.allow_unresolved 170 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 171 | Hocon::Impl::ConfigImpl.trace( 172 | "caching #{full_key} result #{resolved}", 173 | depth) 174 | end 175 | 176 | with_memo = with_memo.memoize(full_key, resolved) 177 | else 178 | raise ConfigBugOrBrokenError.new( 179 | "resolve_substitutions did not give us a resolved object") 180 | end 181 | end 182 | Hocon::Impl::ResolveResult.make(with_memo, resolved) 183 | end 184 | end 185 | 186 | # This method is a translation of the constructor in the Java version with signature 187 | # ResolveContext(ConfigResolveOptions options, Path restrictToChild) 188 | def self.construct(options, restrict_to_child) 189 | context = self.new(Hocon::Impl::ResolveMemos.new, 190 | options, 191 | restrict_to_child, 192 | [], 193 | new_cycle_markers) 194 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 195 | Hocon::Impl::ConfigImpl.trace( 196 | "ResolveContext restrict to child #{restrict_to_child}", context.depth) 197 | end 198 | context 199 | end 200 | 201 | def trace_string 202 | separator = ", " 203 | sb = "" 204 | @resolve_stack.each { |value| 205 | if value.instance_of?(Hocon::Impl::ConfigReference) 206 | sb << value.expression.to_s 207 | sb << separator 208 | end 209 | } 210 | if sb.length > 0 211 | sb.chomp!(separator) 212 | end 213 | sb 214 | end 215 | 216 | def depth 217 | if @resolve_stack.size > 30 218 | raise Hocon::ConfigError::ConfigBugOrBrokenError.new("resolve getting too deep") 219 | end 220 | @resolve_stack.size 221 | end 222 | 223 | def self.resolve(value, root, options) 224 | source = Hocon::Impl::ResolveSource.new(root) 225 | context = construct(options, nil) 226 | begin 227 | context.resolve(value, source).value 228 | rescue NotPossibleToResolve => e 229 | # ConfigReference was supposed to catch NotPossibleToResolve 230 | raise ConfigBugOrBrokenError( 231 | "NotPossibleToResolve was thrown from an outermost resolve", e) 232 | end 233 | end 234 | 235 | def pop_trace 236 | copy = @resolve_stack.clone 237 | old = copy.delete_at(@resolve_stack.size - 1) 238 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 239 | Hocon::Impl::ConfigImpl.trace("popped trace #{old}", depth - 1) 240 | end 241 | Hocon::Impl::ResolveContext.new(@memos, @options, @restrict_to_child, copy, @cycle_markers) 242 | end 243 | 244 | private 245 | 246 | def push_trace(value) 247 | if Hocon::Impl::ConfigImpl.trace_substitution_enabled 248 | Hocon::Impl::ConfigImpl.trace("pushing trace #{value}", depth) 249 | end 250 | copy = @resolve_stack.clone 251 | copy << value 252 | Hocon::Impl::ResolveContext.new(@memos, @options, @restrict_to_child, copy, @cycle_markers) 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/hocon/impl/path_parser.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'stringio' 4 | require_relative '../../hocon/impl' 5 | require_relative '../../hocon/config_syntax' 6 | require_relative '../../hocon/impl/tokenizer' 7 | require_relative '../../hocon/impl/config_node_path' 8 | require_relative '../../hocon/impl/tokens' 9 | require_relative '../../hocon/config_value_type' 10 | require_relative '../../hocon/config_error' 11 | 12 | class Hocon::Impl::PathParser 13 | ConfigSyntax = Hocon::ConfigSyntax 14 | SimpleConfigOrigin = Hocon::Impl::SimpleConfigOrigin 15 | Tokenizer = Hocon::Impl::Tokenizer 16 | Tokens = Hocon::Impl::Tokens 17 | ConfigNodePath = Hocon::Impl::ConfigNodePath 18 | ConfigValueType = Hocon::ConfigValueType 19 | ConfigBadPathError = Hocon::ConfigError::ConfigBadPathError 20 | 21 | 22 | 23 | class Element 24 | def initialize(initial, can_be_empty) 25 | @can_be_empty = can_be_empty 26 | @sb = StringIO.new(initial) 27 | end 28 | 29 | attr_accessor :can_be_empty, :sb 30 | 31 | def to_string 32 | "Element(#{@sb.string},#{@can_be_empty})" 33 | end 34 | end 35 | 36 | def self.api_origin 37 | SimpleConfigOrigin.new_simple("path parameter") 38 | end 39 | 40 | def self.parse_path_node(path, flavor = ConfigSyntax::CONF) 41 | reader = StringIO.new(path) 42 | 43 | begin 44 | tokens = Tokenizer.tokenize(api_origin, reader, 45 | flavor) 46 | tokens.next # drop START 47 | parse_path_node_expression(tokens, api_origin, path, flavor) 48 | ensure 49 | reader.close 50 | end 51 | end 52 | 53 | def self.parse_path(path) 54 | speculated = speculative_fast_parse_path(path) 55 | if not speculated.nil? 56 | return speculated 57 | end 58 | 59 | reader = StringIO.new(path) 60 | 61 | begin 62 | tokens = Tokenizer.tokenize(api_origin, reader, ConfigSyntax::CONF) 63 | tokens.next # drop START 64 | return parse_path_expression(tokens, api_origin, path) 65 | ensure 66 | reader.close 67 | end 68 | end 69 | 70 | def self.parse_path_node_expression(expression, origin, original_text = nil, 71 | flavor = ConfigSyntax::CONF) 72 | path_tokens = [] 73 | path = parse_path_expression(expression, origin, original_text, path_tokens, flavor) 74 | ConfigNodePath.new(path, path_tokens); 75 | end 76 | 77 | def self.parse_path_expression(expression, origin, original_text = nil, 78 | path_tokens = nil, flavor = ConfigSyntax::CONF) 79 | # each builder in "buf" is an element in the path 80 | buf = [] 81 | buf.push(Element.new("", false)) 82 | 83 | if !expression.has_next? 84 | raise ConfigBadPathError.new( 85 | origin, 86 | original_text, 87 | "Expecting a field name or path here, but got nothing") 88 | end 89 | 90 | while expression.has_next? 91 | t = expression.next 92 | 93 | if ! path_tokens.nil? 94 | path_tokens << t 95 | end 96 | 97 | # Ignore all IgnoredWhitespace tokens 98 | next if Tokens.ignored_whitespace?(t) 99 | 100 | if Tokens.value_with_type?(t, ConfigValueType::STRING) 101 | v = Tokens.value(t) 102 | # this is a quoted string; so any periods 103 | # in here don't count as path separators 104 | s = v.transform_to_string 105 | add_path_text(buf, true, s) 106 | elsif t == Tokens::EOF 107 | # ignore this; when parsing a file, it should not happen 108 | # since we're parsing a token list rather than the main 109 | # token iterator, and when parsing a path expression from the 110 | # API, it's expected to have an EOF. 111 | else 112 | # any periods outside of a quoted string count as 113 | # separators 114 | text = nil 115 | if Tokens.value?(t) 116 | # appending a number here may add 117 | # a period, but we _do_ count those as path 118 | # separators, because we basically want 119 | # "foo 3.0bar" to parse as a string even 120 | # though there's a number in it. The fact that 121 | # we tokenize non-string values is largely an 122 | # implementation detail. 123 | v = Tokens.value(t) 124 | 125 | # We need to split the tokens on a . so that we can get sub-paths but still preserve 126 | # the original path text when doing an insertion 127 | if ! path_tokens.nil? 128 | path_tokens.delete_at(path_tokens.size - 1) 129 | path_tokens.concat(split_token_on_period(t, flavor)) 130 | end 131 | text = v.transform_to_string 132 | elsif Tokens.unquoted_text?(t) 133 | # We need to split the tokens on a . so that we can get sub-paths but still preserve 134 | # the original path text when doing an insertion on ConfigNodeObjects 135 | if ! path_tokens.nil? 136 | path_tokens.delete_at(path_tokens.size - 1) 137 | path_tokens.concat(split_token_on_period(t, flavor)) 138 | end 139 | text = Tokens.unquoted_text(t) 140 | else 141 | raise ConfigBadPathError.new( 142 | origin, 143 | original_text, 144 | "Token not allowed in path expression: #{t}" + 145 | " (you can double-quote this token if you really want it here)") 146 | end 147 | 148 | add_path_text(buf, false, text) 149 | end 150 | end 151 | 152 | pb = Hocon::Impl::PathBuilder.new 153 | buf.each do |e| 154 | if (e.sb.length == 0) && !e.can_be_empty 155 | raise Hocon::ConfigError::ConfigBadPathError.new( 156 | origin, 157 | original_text, 158 | "path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)") 159 | else 160 | pb.append_key(e.sb.string) 161 | end 162 | end 163 | 164 | pb.result 165 | end 166 | 167 | def self.split_token_on_period(t, flavor) 168 | token_text = t.token_text 169 | if token_text == "." 170 | return [t] 171 | end 172 | split_token = token_text.split('.') 173 | split_tokens = [] 174 | split_token.each do |s| 175 | if flavor == ConfigSyntax::CONF 176 | split_tokens << Tokens.new_unquoted_text(t.origin, s) 177 | else 178 | split_tokens << Tokens.new_string(t.origin, s, "\"#{s}\"") 179 | end 180 | split_tokens << Tokens.new_unquoted_text(t.origin, ".") 181 | end 182 | if token_text[-1] != "." 183 | split_tokens.delete_at(split_tokens.size - 1) 184 | end 185 | split_tokens 186 | end 187 | 188 | def self.add_path_text(buf, was_quoted, new_text) 189 | i = if was_quoted 190 | -1 191 | else 192 | new_text.index('.') || -1 193 | end 194 | current = buf.last 195 | if i < 0 196 | # add to current path element 197 | current.sb << new_text 198 | # any empty quoted string means this element can 199 | # now be empty. 200 | if was_quoted && (current.sb.length == 0) 201 | current.can_be_empty = true 202 | end 203 | else 204 | # "buf" plus up to the period is an element 205 | current.sb << new_text[0, i] 206 | # then start a new element 207 | buf.push(Element.new("", false)) 208 | # recurse to consume remainder of new_text 209 | add_path_text(buf, false, new_text[i + 1, new_text.length - 1]) 210 | end 211 | end 212 | 213 | # the idea is to see if the string has any chars or features 214 | # that might require the full parser to deal with. 215 | def self.looks_unsafe_for_fast_parser(s) 216 | last_was_dot = true # // start of path is also a "dot" 217 | len = s.length 218 | if s.empty? 219 | return true 220 | end 221 | if s[0] == "." 222 | return true 223 | end 224 | if s[len - 1] == "." 225 | return true 226 | end 227 | 228 | (0..len).each do |i| 229 | c = s[i] 230 | if c =~ /^\w$/ 231 | last_was_dot = false 232 | next 233 | elsif c == '.' 234 | if last_was_dot 235 | return true # ".." means we need to throw an error 236 | end 237 | last_was_dot = true 238 | elsif c == '-' 239 | if last_was_dot 240 | return true 241 | end 242 | next 243 | else 244 | return true 245 | end 246 | end 247 | 248 | if last_was_dot 249 | return true 250 | end 251 | 252 | false 253 | end 254 | 255 | def self.fast_path_build(tail, s, path_end) 256 | # rindex takes last index it should look at, end - 1 not end 257 | split_at = s.rindex(".", path_end - 1) 258 | tokens = [] 259 | tokens << Tokens.new_unquoted_text(nil, s) 260 | # this works even if split_at is -1; then we start the substring at 0 261 | with_one_more_element = Path.new(s[split_at + 1..path_end], tail) 262 | if split_at < 0 263 | with_one_more_element 264 | else 265 | fast_path_build(with_one_more_element, s, split_at) 266 | end 267 | end 268 | 269 | # do something much faster than the full parser if 270 | # we just have something like "foo" or "foo.bar" 271 | def self.speculative_fast_parse_path(path) 272 | s = Hocon::Impl::ConfigImplUtil.unicode_trim(path) 273 | if looks_unsafe_for_fast_parser(s) 274 | return nil 275 | end 276 | 277 | fast_path_build(nil, s, s.length) 278 | end 279 | 280 | end 281 | --------------------------------------------------------------------------------