Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add INI.build #5298

Merged
merged 3 commits into from Jan 2, 2018
Merged

Add INI.build #5298

merged 3 commits into from Jan 2, 2018

Conversation

j8r
Copy link
Contributor

@j8r j8r commented Nov 15, 2017

Implements a basic method to generate an INI-style configuration from a hash.

@j8r j8r force-pushed the ini-build branch 3 times, most recently from c1e3b2c to 077dd10 Compare November 15, 2017 15:38
src/ini.cr Outdated
io = IO::Memory.new
ini.each do |key, value|
io << "[" << key << "]\n"
value.each do |str|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do .each do |key, value| here and rename the existing key to section and the existing value to contents.

@jhass
Copy link
Member

jhass commented Nov 15, 2017

Oh and I just noticed that using single quotes over double quotes where you can (Chars over Strings), should give a slight performance win :)

@j8r
Copy link
Contributor Author

j8r commented Nov 15, 2017

@jhass Thanks for the tips, I've done the changes.

src/ini.cr Outdated
contents.each do |key, value|
io << key << space << '=' << space << value << "\n"
end
io << "\n"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last one, promise, Crystal actually interprets \n within single quotes since they're used to distinguish the type, not whether escape sequences are interpreted ;)


describe "build from Hash(String, Hash(String, String)" do
it "build file" do
INI.build(INI.parse(File.read "#{__DIR__}/data/test_file.ini"), " ").should eq(File.read "#{__DIR__}/data/test_file.ini")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add an actual example in code which does not rely on the INI.parse and the contents of an additional file. The example in the docstring would do.

src/ini.cr Outdated
# ```
# INI.build({"foo" => {"a" => "1"}}, " ") # => "[foo]\na = 1\n\n"
# ```
def self.build(ini : Hash(String, Hash(String, String)), space : String = "") : String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a configurable space really useful? And why is it not a single whitespace by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do this because there are INI files with and without spaces around =. It's not clear what is the good default. For example, systemd services has without spaces and php.ini has one space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space should be IMO " " (space!) from default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method (and the overload below) should also take NamedTuple(String, String) as an ini argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to decide between no spaces and one space. By looking around, the specifications of Microsoft and Cloanto tend to use no spaces, but Apache and PHP use one around =.
Anyone has others official sources for INI specifications?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, removing the type restriction from the method signature, the code should instantly work for named tuples and other payload types than String.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add Boolean, Number, String according to the different implementations listed in Wikipedia. Array isn't always supported.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, just remove all type restrictions. This way, you can pass in any type and the builder just prints the result of to_s. This is the most developer-friendly solution.

Copy link
Contributor Author

@j8r j8r Nov 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@straight-shoota I know, but adding NamedTuple(String, String) raises can only instantiate NamedTuple with named arguments.

By the way regardless of the types, the mapping should follow the Key(Value, Key(Key, Value) structure.

That's OK, we will just need to add verifications and raise custom errors to avoid incorrect INI files if we accept everything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do it like this:

module INI
  def self.build(io : IO, ini, space : String = "")
    ini.each do |section, contents|
      io << '[' << section << "]\n"
       contents.each do |key, value|
         io << key << space << '=' << space << value << '\n'
       end
       io.puts
     end
  end
end

INI.build(STDOUT, {"main": {foo: "bar", baz: 12, regex: /regex/}})

No need for type restrictions or custom errors. If a data structure that does not provide a matching interface, it will automatically result in a compile time error.

src/ini.cr Outdated
# INI.build({"foo" => {"a" => "1"}}, " ") # => "[foo]\na = 1\n\n"
# ```
def self.build(ini : Hash(String, Hash(String, String)), space : String = "") : String
io = IO::Memory.new
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use a String::Builder instead of Memory::IO, because this is optimized for creating a string.
And please add a method overload INI.build(io : IO, hash) that can directly write to an IO instead of allocating a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've first found IO::Memory.new if the Crystal performance guide. So we can further improve perfomance using String::Builder for strings; I will make a PR for this, thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not found yet a case when IO::Memory.new is slower than the String.build approach, using Benchmark.ips. Anyone has a sample to demonstrate this? We will be able to add it to the Performance Guide.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require "benchmark"

Benchmark.ips do |x|
  x.report("String.build") do
    String.build do |io|
      100.times do
        io << "hello world"
      end
    end
  end
  x.report("IO::Memory") do
    io = IO::Memory.new
    100.times do
      io << "hello world"
    end
    io.to_s
  end
end

Output:

String.build 475.55k (   2.1µs) (± 1.25%)       fastest
  IO::Memory  252.7k (  3.96µs) (± 1.47%)  1.88× slower

whats your benchmark?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@larubujo Thanks! I haven't used 100.times before. I had just added a block similar to ones already present in the guide. So I've faced strange issues, memory related I think.

src/ini.cr Outdated
contents.each do |key, value|
io << key << space << '=' << space << value << '\n'
end
io << '\n'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line could be replaced by io.puts

@j8r j8r force-pushed the ini-build branch 2 times, most recently from e1363cd to b16aef1 Compare November 15, 2017 20:27
src/ini.cr Outdated
# INI.build({"foo" => {"a" => "1"}}, " ") # => "[foo]\na = 1\n\n"
# ```
def self.build(ini : Hash(String, Hash(String, String)), space : String = "") : String
str = String::Builder.new
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better use String.build instead of instantiating a builder directly.

src/ini.cr Outdated
# ```
def self.build(ini : Hash(String, Hash(String, String)), space : String = "") : String
String.build do |str|
build str, ini, " "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last argument should be space

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, miss this one

src/ini.cr Outdated

# Appends INI data to the given IO.
#
def self.build(io : IO, ini : Hash(String, Hash(String, String)), space : String = "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -1,6 +1,53 @@
require "spec"
require "ini"

class INI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in the spec??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I've pasted this by mistake. I revert this ASAP

@@ -1,8 +1,8 @@
[general]
log_level = DEBUG
log_level = D
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better not to change existing test data. You don't really need to use this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's for Char type testing, that should be converted to a String.

src/ini.cr Outdated

# Appends INI data to the given IO.
#
def self.build(io : IO, ini, space : String = "")
Copy link
Contributor

@RX14 RX14 Nov 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think that having the space be configurable as any string is a bit too much and makes it possible to create invalid INI files. Why not make it a boolean which switches between "" and " ". Regardless, I think 1 space should be the default, not zero. We can always change this default later. Actually nvm, let's maap whitespace false by default.

src/ini.cr Outdated
ini.each do |section, contents|
io << '[' << section << "]\n"
contents.each do |key, value|
io << key << space << '=' << space << value << '\n'
io << key << s << '=' << s << value << '\n'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather

io << key
io << ' ' if space
io << '='
io << ' ' if space
io << value << '\n'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done it with io << key << (space ? " = " : '=') << value << '\n', less verbose. Is it fine?

@j8r j8r force-pushed the ini-build branch 2 times, most recently from 5d38bf3 to 6bf43c5 Compare November 18, 2017 20:08
@straight-shoota
Copy link
Member

Please add some specs for space = false. This can be done simple with a short string, no need to create an additional file.

src/ini.cr Outdated
end

# Appends INI data to the given IO.
#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant blank line.

@j8r j8r force-pushed the ini-build branch 3 times, most recently from 2030d85 to e486db7 Compare November 19, 2017 12:02
@RX14 RX14 added this to the Next milestone Jan 2, 2018
@RX14 RX14 merged commit 797a2a6 into crystal-lang:master Jan 2, 2018
@j8r j8r deleted the ini-build branch January 4, 2018 09:22
lukeasrodgers pushed a commit to lukeasrodgers/crystal that referenced this pull request Jan 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants