pax_global_header00006660000000000000000000000064137726705230014526gustar00rootroot0000000000000052 comment=f2f1d7e1a18abb180e3e96980549359069e21446 necromancer-0.7.0/000077500000000000000000000000001377267052300140265ustar00rootroot00000000000000necromancer-0.7.0/.editorconfig000066400000000000000000000002261377267052300165030ustar00rootroot00000000000000root = true [*.rb] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true necromancer-0.7.0/.github/000077500000000000000000000000001377267052300153665ustar00rootroot00000000000000necromancer-0.7.0/.github/FUNDING.yml000066400000000000000000000000241377267052300171770ustar00rootroot00000000000000github: piotrmurach necromancer-0.7.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000010501377267052300200670ustar00rootroot00000000000000### Are you in the right place? * For issues or feature requests file a GitHub issue in this repository * For general questions or discussion post on StackOverflow ### Describe the problem A brief description of the issue/feature. ### Steps to reproduce the problem ``` Your code here to reproduce the issue ``` ### Actual behaviour What happened? This could be a description, log output, error raised etc... ### Expected behaviour What did you expect to happen? ### Describe your environment * OS version: * Ruby version: * Necromancer version: necromancer-0.7.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000007141377267052300211710ustar00rootroot00000000000000### Describe the change What does this Pull Request do? ### Why are we doing this? Any related context as to why is this is a desirable change. ### Benefits How will the library improve? ### Drawbacks Possible drawbacks applying this change. ### Requirements Put an X between brackets on each line if you have done the item: - [ ] Tests written & passing locally? - [ ] Code style checked? - [ ] Rebased with `master` branch? - [ ] Documentation updated? necromancer-0.7.0/.github/workflows/000077500000000000000000000000001377267052300174235ustar00rootroot00000000000000necromancer-0.7.0/.github/workflows/ci.yml000066400000000000000000000023241377267052300205420ustar00rootroot00000000000000--- name: CI on: push: branches: - master paths-ignore: - "bin/**" - "*.md" pull_request: branches: - master paths-ignore: - "bin/**" - "*.md" jobs: tests: name: Ruby ${{ matrix.ruby }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest ruby: - 2.1 - 2.2 - 2.3 - 2.4 - 2.5 - 2.6 - 3.0 - ruby-head - jruby-9.2.13.0 - jruby-head - truffleruby-head include: - ruby: 2.7 os: ubuntu-latest coverage: true env: COVERAGE: ${{ matrix.coverage }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Install bundler run: gem install bundler -v '< 2.0' - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake ci necromancer-0.7.0/.gitignore000066400000000000000000000001661377267052300160210ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log necromancer-0.7.0/.rspec000066400000000000000000000000511377267052300151370ustar00rootroot00000000000000--color --require spec_helper --warnings necromancer-0.7.0/.rubocop.yml000066400000000000000000000015451377267052300163050ustar00rootroot00000000000000AllCops: NewCops: enable Layout/FirstArrayElementIndentation: EnforcedStyle: consistent Layout/HashAlignment: EnforcedHashRocketStyle: table Layout/LineLength: Max: 82 Lint/AssignmentInCondition: Enabled: false Metrics/AbcSize: Max: 30 Metrics/BlockLength: CountComments: true Max: 25 IgnoredMethods: [] Exclude: - "spec/**/*" Metrics/ClassLength: Max: 1500 Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Max: 20 Naming/BinaryOperatorParameterName: Enabled: false Style/AccessorGrouping: Enabled: false Style/AsciiComments: Enabled: false Style/LambdaCall: EnforcedStyle: braces Style/StringLiterals: EnforcedStyle: double_quotes Style/TrivialAccessors: Enabled: false # { ... } for multi-line blocks is okay Style/BlockDelimiters: Enabled: false Style/CommentedKeyword: Enabled: false necromancer-0.7.0/CHANGELOG.md000066400000000000000000000045761377267052300156530ustar00rootroot00000000000000# Change log ## [v0.7.0] - 2020-12-29 ### Added * Add HashConverters for transforming string into hash of string, integer, float or boolean values * Add converters for transforming string to array of booleans, integers, floats and numeric ### Changed * Change StringToRange converter to work with decimal numbers and spaces * Change :strict to be a keyword argument * Change StringToNumeric converter to allow numbers with space characters ## [v0.6.0] - 2020-03-08 ### Changed * Change gemspec to remove test artifacts ## [v0.5.1] - 2019-11-24 ### Changed * Change gemspec to include metadata ### Fixed * Fix Ruby 2.7 warnings by Ryan Davis(@zenspider) ## [v0.5.0] - 2019-03-23 ### Changed * Change to use Ruby >= 2.0.0 * Change to load files directly without git * Change to raise on error in place of fail ## [v0.4.0] - 2017-02-18 ### Added * Add :string -> :time conversion * Add inspection methods to Context and ConversionTarget * Add module level Necromancer#convert for convenience and more functional style * Add ConversionTarget#>> call for functional style converions ### Changed * Change fail to raise in ConversionTarget#for * Change fail to raise in Conversions * Change ConversionTarget#detect to handle Class type coercion ### Fixed * Fix bug with type detection * Fix Ruby 2.4.0 warning about Fixnum & Bignum type ## [v0.3.0] - 2014-12-14 ### Added * Add array converters for :hash, :set and :object conversions * Add ability to configure global conversion settings per instance ## [v0.2.0] - 2014-12-07 ### Added * Add #fail_conversion_type to Converter and use in converters * Add DateTimeConverters * Add string to numeric type conversion ### Changed * Change IntegerConverters & FloatConverters into Numeric Converters ## [v0.1.0] - 2014-11-30 * Initial implementation and release [v0.7.0]: https://github.com/piotrmurach/necromancer/compare/v0.6.0...v0.7.0 [v0.6.0]: https://github.com/piotrmurach/necromancer/compare/v0.5.1...v0.6.0 [v0.5.1]: https://github.com/piotrmurach/necromancer/compare/v0.5.0...v0.5.1 [v0.5.0]: https://github.com/piotrmurach/necromancer/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/piotrmurach/necromancer/compare/v0.3.0...v0.4.0 [v0.3.0]: https://github.com/piotrmurach/necromancer/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/piotrmurach/necromancer/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/piotrmurach/necromancer/compare/v0.1.0 necromancer-0.7.0/CODE_OF_CONDUCT.md000066400000000000000000000062401377267052300166270ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at piotr@piotrmurach.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ necromancer-0.7.0/Gemfile000066400000000000000000000003521377267052300153210ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem "simplecov", "~> 0.16.1" gem "coveralls", "~> 0.8.22" gem "yardstick", "~> 0.9.9" gem "json", "2.4.1" if RUBY_VERSION == "2.0.0" group :development do gem "benchmark-ips", "~> 2.7.2" end necromancer-0.7.0/LICENSE.txt000066400000000000000000000020551377267052300156530ustar00rootroot00000000000000Copyright (c) 2014 Piotr Murach MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. necromancer-0.7.0/README.md000066400000000000000000000370201377267052300153070ustar00rootroot00000000000000# Necromancer [![Gem Version](https://badge.fury.io/rb/necromancer.svg)][gem] [![Actions CI](https://github.com/piotrmurach/tty-runner/workflows/CI/badge.svg?branch=master)][gh_actions_ci] [![Build status](https://ci.appveyor.com/api/projects/status/qj3xn5gbbfi4puet?svg=true)][appveyor] [![Code Climate](https://codeclimate.com/github/piotrmurach/necromancer/badges/gpa.svg)][codeclimate] [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/necromancer/badge.svg?branch=master)][coverage] [![Inline docs](http://inch-ci.org/github/piotrmurach/necromancer.svg?branch=master)][inchpages] [gem]: http://badge.fury.io/rb/necromancer [gh_actions_ci]: https://github.com/piotrmurach/tty-runner/actions?query=workflow%3ACI [appveyor]: https://ci.appveyor.com/project/piotrmurach/necromancer [codeclimate]: https://codeclimate.com/github/piotrmurach/necromancer [coverage]: https://coveralls.io/github/piotrmurach/necromancer [inchpages]: http://inch-ci.org/github/piotrmurach/necromancer > Conversion from one object type to another with a bit of black magic. **Necromancer** provides independent type conversion component for [TTY](https://github.com/piotrmurach/tty) toolkit. ## Motivation Conversion between Ruby core types frequently comes up in projects but is solved by half-baked solutions. This library aims to provide an independent and extensible API to support a robust and generic way to convert between core Ruby types. ## Features * Simple and expressive API * Ability to specify own converters * Ability to compose conversions out of simpler ones * Support conversion of custom defined types * Ability to specify strict conversion mode ## Installation Add this line to your application's Gemfile: ```ruby gem "necromancer" ``` And then execute: $ bundle Or install it yourself as: $ gem install necromancer ## Contents * [1. Usage](#1-usage) * [2. Interface](#2-interface) * [2.1 convert](#21-convert) * [2.2 from](#22-from) * [2.3 to](#23-to) * [2.4 can?](#24-can) * [2.5 configure](#25-configure) * [3. Converters](#3-converters) * [3.1 Array](#31-array) * [3.2 Boolean](#32-boolean) * [3.3 DateTime](#33-datetime) * [3.4 Hash](#34-hash) * [3.5 Numeric](#35-numeric) * [3.6 Range](#36-range) * [3.7 Custom](#37-custom) * [3.7.1 Using an Object](#371-using-an-object) * [3.7.2 Using a Proc](#372-using-a-proc) ## 1. Usage **Necromancer** knows how to handle conversions between various types using the `convert` method. The `convert` method takes as an argument the value to convert from. Then to perform actual coercion use the `to` or more functional style `>>` method that accepts the type for the returned value which can be `:symbol`, `object` or `ClassName`. For example, to convert a string to a [range](#36-range) type: ```ruby Necromancer.convert("1-10").to(:range) # => 1..10 Necromancer.convert("1-10") >> :range # => 1..10 Necromancer.convert("1-10") >> Range # => 1..10 ``` In order to handle [boolean](#32-boolean) conversions: ```ruby Necromancer.convert("t").to(:boolean) # => true Necromancer.convert("t") >> true # => true ``` To convert string to [numeric](#35-numeric) value: ```ruby Necromancer.convert("10e1").to(:numeric) # => 100 ``` You can convert string to [array](#31-array) of values like `boolean`, `integer` or `float`: ```ruby Necromancer.convert("t,f,t"]).to(:booleans) # => [true, false, true] Necromancer.convert("1,2.3,3.0"]).to(:integers) # => [1, 2, 3] Necromancer.convert("1,2.3,3.0"]).to(:floats) # => [1.0, 2.3, 3.0] ``` To convert string to [hash](#34-hash) value: ```ruby Necromancer.convert("a:1 b:2 c:3").to(:hash) # => {a: "1", b: "2", c: "3"} Necromancer.convert("a=1 b=2 c=3").to(:hash) # => {a: "1", b: "2", c: "3"} ```` To provide extra information about the conversion value type use the `from`: ```ruby Necromancer.convert(["1", "2.3", "3.0"]).from(:array).to(:numeric) # => [1, 2.3, 3.0] ``` **Necromancer** also allows you to add [custom](#37-custom) conversions. When conversion isn't possible, a `Necromancer::NoTypeConversionAvailableError` is thrown indicating that `convert` doesn't know how to perform the requested conversion: ```ruby Necromancer.convert(:foo).to(:float) # => Necromancer::NoTypeConversionAvailableError: Conversion 'foo->float' unavailable. ``` ## 2. Interface **Necromancer** will perform conversions on the supplied object through use of `convert`, `from` and `to` methods. ### 2.1 convert For the purpose of divination, **Necromancer** uses `convert` method to turn source type into target type. For example, in order to convert a string into a range type do: ```ruby Necromancer.convert("1,10").to(:range) # => 1..10 ``` Alternatively, you can use block: ```ruby Necromancer.convert { "1,10" }.to(:range) # => 1..10 ``` Conversion isn't always possible, in which case a `Necromancer::NoTypeConversionAvailableError` is thrown indicating that `convert` doesn't know how to perform the requested conversion: ```ruby Necromancer.convert(:foo).to(:float) # => Necromancer::NoTypeConversionAvailableError: Conversion 'foo->float' unavailable. ``` ### 2.2 from To specify conversion source type use `from` method: ```ruby Necromancer.convert("1.0").from(:string).to(:numeric) ``` In majority of cases you do not need to specify `from` as the type will be inferred from the `convert` method argument and then appropriate conversion will be applied to result in `target` type such as `:numeric`. However, if you do not control the input to `convert` and want to ensure consistent behaviour please use `from`. The source parameters are: * `:array` * `:boolean` * `:date` * `:datetime` * `:float` * `:integer` * `:numeric` * `:range` * `:string` * `:time` ### 2.3 to To convert objects between types, **Necromancer** provides several target types. The `to` or functional style `>>` method allows you to pass target as an argument to perform actual conversion. The target can be one of `:symbol`, `object` or `ClassName`: ```ruby Necromancer.convert("yes").to(:boolean) # => true Necromancer.convert("yes") >> :boolean # => true Necromancer.convert("yes") >> true # => true Necromancer.convert("yes") >> TrueClass # => true ``` By default, when target conversion fails the original value is returned. However, you can pass `strict` as an additional argument to ensure failure when conversion cannot be performed: ```ruby Necromancer.convert("1a").to(:integer, strict: true) # => raises Necromancer::ConversionTypeError ``` The target parameters are: * `:array` * `:boolean`, `:booleans`, `:bools`, `:boolean_hash`, `:bool_hash` * `:date` * `:datetime`, * `:float`, `:floats`, `:float_hash` * `:integer`, `:integers`, `:ints`, `:integer_hash`, `:int_hash` * `:numeric`, `:numerics`, `:nums`, `:numeric_hash`, `:num_hash` * `:range` * `:string` * `:time` ### 2.4 can? To verify that a given conversion can be handled by **Necromancer** call `can?` with the `source` and `target` of the desired conversion. ```ruby converter = Necromancer.new converter.can?(:string, :integer) # => true converter.can?(:unknown, :integer) # => false ``` ### 2.5 configure You may set global configuration options on **Necromancer** instance by passing a block like so: ```ruby Necromancer.new do |config| config.strict true end ``` Or calling `configure` method: ```ruby converter = Necromancer.new converter.configure do |config| config.copy false end ``` Available configuration options are: * `strict` - ensures correct types for conversion, by default `false` * `copy` - ensures only copy is modified, by default `true` ## 3. Converters **Necromancer** flexibility means you can register your own converters or use the already defined converters for such types as `Array`, `Boolean`, `Date`, `DateTime`, `Hash`, `Numeric`, `Range` and `Time`. ### 3.1 Array The **Necromancer** allows you to transform arbitrary object into array: ```ruby Necromancer.convert(nil).to(:array) # => [] Necromancer.convert({x: 1}).to(:array) # => [[:x, 1]] ``` In addition, **Necromancer** excels at converting `,` or `-` delimited string into an array object: ```ruby Necromancer.convert("a, b, c").to(:array) # => ["a", "b", "c"] ``` If the string is a list of `-` or `,` separated numbers, they will be converted to their respective numeric types: ```ruby Necromancer.convert("1 - 2 - 3").to(:array) # => [1, 2, 3] ``` It handles conversion of string into an array of boolean values as well: ```ruby Necromancer.convert("yes,no,t").to(:booleans) # => [true, false, true] Necromancer.convert("1 - f - FALSE").to(:bools) # => [true, false, false] ``` You can also convert array containing string objects to array containing numeric values: ```ruby Necromancer.convert(["1", "2.3", "3.0"]).to(:numerics) # => [1, 2.3, 3.0] Necromancer.convert(["1", "2.3", "3.0"]).to(:nums) # => [1, 2.3, 3.0] ``` Or you can be more specific by using `:integers` and `:floats` as the resulting type: ```ruby Necromancer.convert(["1", "2.3", "3.0"]).to(:integers) # => [1, 2, 3] ``` When in `strict` mode the conversion will raise a `Necromancer::ConversionTypeError` error like so: ```ruby Necromancer.convert(["1", "2.3", false]).to(:numerics, strict: true) # => Necromancer::ConversionTypeError: false cannot be converted from `array` to `numerics` ``` However, in `non-strict` mode the value will be simply returned unchanged: ```ruby Necromancer.convert(["1", "2.3", false]).to(:numerics, strict: false) # => [1, 2.3, false] ``` ### 3.2 Boolean The **Necromancer** allows you to convert a string object to boolean object. The `1`, `"1"`, `"t"`, `"T"`, `"true"`, `"TRUE"`, `"y"`, `"Y"`, `"yes"`, `"Yes"`, `"on"`, `"ON"` values are converted to `TrueClass`. ```ruby Necromancer.convert("yes").to(:boolean) # => true ``` Similarly, the `0`, `"0"`, `"f"`, `"F"`, `"false"`, `"FALSE"`, `"n"`, `"N"`, `"no"`, `"No"`, `"off"`, `"OFF"` values are converted to `FalseClass`. ```ruby Necromancer.convert("no").to(:boolean) # => false ``` You can also convert an integer object to boolean: ```ruby Necromancer.convert(1).to(:boolean) # => true Necromancer.convert(0).to(:boolean) # => false ``` ### 3.3 DateTime **Necromancer** knows how to convert string to `date` object: ```ruby Necromancer.convert("1-1-2015").to(:date) # => "2015-01-01" Necromancer.convert("01/01/2015").to(:date) # => "2015-01-01" ``` You can also convert string to `datetime`: ```ruby Necromancer.convert("1-1-2015").to(:datetime) # => "2015-01-01T00:00:00+00:00" Necromancer.convert("1-1-2015 15:12:44").to(:datetime) # => "2015-01-01T15:12:44+00:00" ``` To convert a string to a time instance do: ```ruby Necromancer.convert("01-01-2015").to(:time) # => 2015-01-01 00:00:00 +0100 Necromancer.convert("01-01-2015 08:35").to(:time) # => 2015-01-01 08:35:00 +0100 Necromancer.convert("12:35").to(:time) # => 2015-01-04 12:35:00 +0100 ``` ### 3.4 Hash With **Necromancer** you can convert a string with pairs delimited by `=` or `:` characters into a hash: ```ruby Necromancer.convert("a:1 b:2 c:3").to(:hash) Necromancer.convert("a=1 b=2 c=3").to(:hash) # => {a: "1", b: "2", c: "3"} ``` The pairs can be separated by `&` symbols and mix `=` and `:` pair delimiters: ```ruby Necromancer.convert("a:1 & b=2 & c:3").to(:hash) # => {a: "1", b: "2", c: "3"} ``` You can also convert string to hash with integer values using `:int_hash` type: ```ruby Necromancer.convert("a:1 b:2 c:3").to(:int_hash) # => {a: 1, b: 2, c: 3} Necromancer.convert("a:1 b:2 c:3").to(:integer_hash) # => {a: 1, b: 2, c: 3} ``` Similarly you can convert string to hash with `float` or `numeric` values using `:float_hash` and `numeric_hash` types: ```ruby Necromancer.convert("a:1 b:2 c:3").to(:float_hash) # => {a: 1.0, b: 2.0, c: 3.0} Necromancer.convert("a:1 b:2.0 c:3").to(:num_hash) # => {a: 1, b:2.0, c: 3} ``` String can also be converted to hash with boolean values using `:boolean_hash` or `:bool_hash`: ```ruby Necromancer.convert("a:yes b:no c:t").to(:bool_hash) # => {a: true, b: false, c: true} ``` ### 3.5 Numeric **Necromancer** comes ready to convert all the primitive numeric values. To convert a string to a float do: ```ruby Necromancer.convert("1.2a").to(:float) # => 1.2 ``` Conversion to numeric in strict mode raises `Necromancer::ConversionTypeError`: ```ruby Necromancer.convert("1.2a").to(:float, strict: true) # => raises error ``` To convert a string to an integer do: ```ruby Necromancer.convert("1a").to(:integer) # => 1 ``` However, if you want to convert string to an appropriate matching numeric type do: ```ruby Necromancer.convert("1e1").to(:numeric) # => 10 ``` ### 3.6 Range **Necromancer** is no stranger to figuring out ranges from strings. You can pass `,`, `-`, `..`, `...` characters to denote ranges: ```ruby Necromancer.convert("1,10").to(:range) # => 1..10 ``` Or to create a range of letters: ```ruby Necromancer.convert("a-z").to(:range) # => "a".."z" ``` It will handle space characters: ```ruby Necromancer.convert("1 . . 10") >> :range # => 1..10 Necromancer.convert("a . . . z") >> :range # => "a"..."z" ```` ### 3.7 Custom In case where provided conversions do not match your needs you can create your own and `register` with **Necromancer** by using an `Object` or a `Proc`. #### 3.7.1 Using an Object Firstly, you need to create a converter that at minimum requires to specify `call` method that will be invoked during conversion: ```ruby UpcaseConverter = Struct.new(:source, :target) do def call(value, options = {}) value.upcase end end ``` Inside the `UpcaseConverter` you have access to global configuration options by directly calling `config` method. Then you need to specify what type conversions this converter will support. For example, `UpcaseConverter` will allow a string object to be converted to a new string object with content upper cased. This can be done: ```ruby upcase_converter = UpcaseConverter.new(:string, :upcase) ``` **Necromancer** provides the `register` method to add converter: ```ruby converter = Necromancer.new converter.register(upcase_converter) # => true if successfully registered ``` Finally, by invoking `convert` method and specifying `:upcase` as the target for the conversion we achieve the result: ```ruby converter.convert("magic").to(:upcase) # => "MAGIC" ``` #### 3.7.2 Using a Proc Using a Proc object you can create and immediately register a converter. You need to pass `source` and `target` of the conversion that will be used later on to match the conversion. The `convert` allows you to specify the actual conversion in block form. For example: ```ruby converter = Necromancer.new converter.register do |c| c.source= :string c.target= :upcase c.convert = proc { |value, options| value.upcase } end ``` Then by invoking the `convert` method and passing the `:upcase` conversion type you can transform the string like so: ```ruby converter.convert("magic").to(:upcase) # => "MAGIC" ``` ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/necromancer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 1. Fork it ( https://github.com/piotrmurach/necromancer/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ## Copyright Copyright (c) 2014 Piotr Murach. See LICENSE for further details. necromancer-0.7.0/Rakefile000066400000000000000000000002541377267052300154740ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_tasks" FileList["tasks/**/*.rake"].each(&method(:import)) desc "Run all specs" task ci: %w[spec] task default: :spec necromancer-0.7.0/appveyor.yml000066400000000000000000000012541377267052300164200ustar00rootroot00000000000000--- skip_commits: files: - "bin/**" - "*.md" install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - gem install bundler -v '< 2.0' - bundle install --jobs 4 --retry 3 before_test: - ruby -v - gem -v - bundle -v build: off test_script: - bundle exec rake ci environment: matrix: - ruby_version: "200" - ruby_version: "200-x64" - ruby_version: "21" - ruby_version: "21-x64" - ruby_version: "22" - ruby_version: "22-x64" - ruby_version: "23" - ruby_version: "23-x64" - ruby_version: "24" - ruby_version: "24-x64" - ruby_version: "25" - ruby_version: "25-x64" - ruby_version: "26" - ruby_version: "26-x64" necromancer-0.7.0/bin/000077500000000000000000000000001377267052300145765ustar00rootroot00000000000000necromancer-0.7.0/bin/console000077500000000000000000000005321377267052300161660ustar00rootroot00000000000000#!/usr/bin/env ruby require "bundler/setup" require "necromancer" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) necromancer-0.7.0/bin/setup000077500000000000000000000002031377267052300156570ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here necromancer-0.7.0/lib/000077500000000000000000000000001377267052300145745ustar00rootroot00000000000000necromancer-0.7.0/lib/necromancer.rb000066400000000000000000000014761377267052300174250ustar00rootroot00000000000000# frozen_string_literal: true require_relative "necromancer/context" require_relative "necromancer/version" module Necromancer # Raised when cannot conver to a given type ConversionTypeError = Class.new(StandardError) # Raised when conversion type is not available NoTypeConversionAvailableError = Class.new(StandardError) # Create a conversion instance # # @example # converter = Necromancer.new # # @return [Context] # # @api private def new(&block) Context.new(&block) end module_function :new # Convenience to directly call conversion # # @example # Necromancer.convert("1").to(:integer) # # => 1 # # @return [ConversionTarget] # # @api public def convert(*args, &block) Context.new.convert(*args, &block) end module_function :convert end # Necromancer necromancer-0.7.0/lib/necromancer/000077500000000000000000000000001377267052300170705ustar00rootroot00000000000000necromancer-0.7.0/lib/necromancer/configuration.rb000066400000000000000000000015451377267052300222710ustar00rootroot00000000000000# frozen_string_literal: true module Necromancer # A global configuration for converters. # # Used internally by {Necromancer::Context}. # # @api private class Configuration # Configure global strict mode # # @api public attr_writer :strict # Configure global copy mode # # @api public attr_writer :copy # Create a configuration # # @api private def initialize @strict = false @copy = true end # Set or get strict mode # # @return [Boolean] # # @api public def strict(value = (not_set = true)) not_set ? @strict : (self.strict = value) end # Set or get copy mode # # @return [Boolean] # # @api public def copy(value = (not_set = true)) not_set ? @copy : (self.copy = value) end end # Configuration end # Necromancer necromancer-0.7.0/lib/necromancer/context.rb000066400000000000000000000036651377267052300211130ustar00rootroot00000000000000# frozen_string_literal: true require "forwardable" require_relative "configuration" require_relative "conversions" require_relative "conversion_target" module Necromancer # A class used by Necromancer to provide user interace # # @api public class Context extend Forwardable def_delegators :"@conversions", :register # Create a context. # # @api private def initialize(&block) block.(configuration) if block_given? @conversions = Conversions.new(configuration) @conversions.load end # The configuration object. # # @example # converter = Necromancer.new # converter.configuration.strict = true # # @return [Necromancer::Configuration] # # @api public def configuration @configuration ||= Configuration.new end # Yields global configuration to a block. # # @yield [Necromancer::Configuration] # # @example # converter = Necromancer.new # converter.configure do |config| # config.strict true # end # # @api public def configure yield configuration if block_given? end # Converts the object # @param [Object] object # any object to be converted # # @api public def convert(object = ConversionTarget::UndefinedValue, &block) ConversionTarget.for(conversions, object, block) end # Check if this converter can convert source to target # # @param [Object] source # the source class # @param [Object] target # the target class # # @return [Boolean] # # @api public def can?(source, target) !conversions[source, target].nil? rescue NoTypeConversionAvailableError false end # Inspect this context # # @api public def inspect %(#<#{self.class}@#{object_id} @config=#{configuration}>) end protected attr_reader :conversions end # Context end # Necromancer necromancer-0.7.0/lib/necromancer/conversion_target.rb000066400000000000000000000043561377267052300231600ustar00rootroot00000000000000# frozen_string_literal: true module Necromancer # A class responsible for wrapping conversion target # # @api private class ConversionTarget # Used as a stand in for lack of value # @api private UndefinedValue = Module.new # @api private def initialize(conversions, object) @object = object @conversions = conversions end # @api private def self.for(context, value, block) if UndefinedValue.equal?(value) unless block raise ArgumentError, "You need to pass either argument or a block to `convert`." end new(context, block.call) elsif block raise ArgumentError, "You cannot pass both an argument and a block to `convert`." else new(context, value) end end # Allows to specify conversion source type # # @example # converter.convert("1").from(:string).to(:numeric) # => 1 # # @return [ConversionType] # # @api public def from(source) @source = source self end # Runs a given conversion # # @example # converter.convert("1").to(:numeric) # => 1 # # @example # converter.convert("1") >> Integer # => 1 # # @return [Object] # the converted target type # # @api public def to(target, options = {}) conversion = conversions[source || detect(object, false), detect(target)] conversion.call(object, **options) end alias >> to # Inspect this conversion # # @api public def inspect %(#<#{self.class}@#{object_id} @object=#{object}, @source=#{detect(object)}>) end protected # Detect object type and coerce into known key type # # @param [Object] object # # @api private def detect(object, symbol_as_object = true) case object when TrueClass, FalseClass then :boolean when Integer then :integer when Class then object.name.downcase else if object.is_a?(Symbol) && symbol_as_object object else object.class.name.downcase end end end attr_reader :object attr_reader :conversions attr_reader :source end # ConversionTarget end # Necromancer necromancer-0.7.0/lib/necromancer/conversions.rb000066400000000000000000000061371377267052300217740ustar00rootroot00000000000000# frozen_string_literal: true require_relative "configuration" require_relative "converter" require_relative "converters/array" require_relative "converters/boolean" require_relative "converters/date_time" require_relative "converters/hash" require_relative "converters/numeric" require_relative "converters/range" module Necromancer # Represents the context used to configure various converters # and facilitate type conversion # # @api public class Conversions DELIMITER = "->" # Creates a new conversions map # # @example # conversion = Necromancer::Conversions.new # # @api public def initialize(configuration = Configuration.new, map = {}) @configuration = configuration @converter_map = map.dup end # Load converters # # @api private def load ArrayConverters.load(self) BooleanConverters.load(self) DateTimeConverters.load(self) HashConverters.load(self) NumericConverters.load(self) RangeConverters.load(self) end # Retrieve converter for source and target # # @param [Object] source # the source of conversion # # @param [Object] target # the target of conversion # # @return [Converter] # the converter for the source and target # # @api public def [](source, target) key = "#{source}#{DELIMITER}#{target}" converter_map[key] || converter_map["object#{DELIMITER}#{target}"] || raise_no_type_conversion_available(key) end alias fetch [] # Register a converter # # @example with simple object # conversions.register NullConverter.new(:array, :array) # # @example with block # conversions.register do |c| # c.source = :array # c.target = :array # c.convert = -> { |val, options| val } # end # # @api public def register(converter = nil, &block) converter ||= Converter.create(&block) key = generate_key(converter) converter = add_config(converter, @configuration) return false if converter_map.key?(key) converter_map[key] = converter true end # Export all the conversions as hash # # @return [Hash[String, String]] # # @api public def to_hash converter_map.dup end protected # Fail with conversion error # # @api private def raise_no_type_conversion_available(key) raise NoTypeConversionAvailableError, "Conversion '#{key}' unavailable." end # @api private def generate_key(converter) parts = [] parts << (converter.source ? converter.source.to_s : "none") parts << (converter.target ? converter.target.to_s : "none") parts.join(DELIMITER) end # Inject config into converter # # @api private def add_config(converter, config) converter.instance_exec(:"@config") do |var| instance_variable_set(var, config) end converter end # Map of type and conversion # # @return [Hash] # # @api private attr_reader :converter_map end # Conversions end # Necromancer necromancer-0.7.0/lib/necromancer/converter.rb000066400000000000000000000025471377267052300214340ustar00rootroot00000000000000# frozen_string_literal: true require_relative "configuration" module Necromancer # Abstract converter used internally as a base for other converters # # @api private class Converter # Create an abstract converter # # @param [Object] source # the source object type # # @param [Object] target # the target object type # # @api public def initialize(source = nil, target = nil) @source = source if source @target = target if target @config ||= Configuration.new end # Run converter # # @api private def call(*) raise NotImplementedError end # Creates anonymous converter # # @api private def self.create(&block) Class.new(self) do define_method(:initialize) { |*a| block.(self, *a) } define_method(:call) { |value| convert.(value) } end.new end # Fail with conversion type error # # @param [Object] value # the value that cannot be converted # # @api private def raise_conversion_type(value) raise ConversionTypeError, "'#{value}' could not be converted " \ "from `#{source}` into `#{target}`" end attr_accessor :source attr_accessor :target attr_accessor :convert # protected attr_reader :config end # Converter end # Necromancer necromancer-0.7.0/lib/necromancer/converters/000077500000000000000000000000001377267052300212625ustar00rootroot00000000000000necromancer-0.7.0/lib/necromancer/converters/array.rb000066400000000000000000000157701377267052300227370ustar00rootroot00000000000000# frozen_string_literal: true require "set" require_relative "../converter" require_relative "boolean" require_relative "numeric" module Necromancer # Container for Array converter classes module ArrayConverters ARRAY_MATCHER = /^(.+?(\s*(?(,|-))\s*))+/x.freeze # An object that converts a String to an Array class StringToArrayConverter < Converter # Convert string value to array # # @example # converter.call("a, b, c") # => ["a", "b", "c"] # # @example # converter.call("1 - 2 - 3") # => ["1", "2", "3"] # # @api public def call(value, strict: config.strict) return [] if value.to_s.empty? if match = value.to_s.match(ARRAY_MATCHER) value.to_s.split(match[:sep]) else strict ? raise_conversion_type(value) : Array(value) end end end class StringToBooleanArrayConverter < Converter # @example # converter.call("t,f,yes,no") # => [true, false, true, false] # # @param [Array] value # the array value to boolean # # @api public def call(value, strict: config.strict) array_converter = StringToArrayConverter.new(:string, :array) array = array_converter.(value, strict: strict) bool_converter = ArrayToBooleanArrayConverter.new(:array, :boolean) bool_converter.(array, strict: strict) end end class StringToIntegerArrayConverter < Converter # @example # converter.call("1,2,3") # => [1, 2, 3] # # @api public def call(string, strict: config.strict) array_converter = StringToArrayConverter.new(:string, :array) array = array_converter.(string, strict: strict) int_converter = ArrayToIntegerArrayConverter.new(:array, :integers) int_converter.(array, strict: strict) end end class StringToFloatArrayConverter < Converter # @example # converter.call("1,2,3") # => [1.0, 2.0, 3.0] # # @api public def call(string, strict: config.strict) array_converter = StringToArrayConverter.new(:string, :array) array = array_converter.(string, strict: strict) float_converter = ArrayToFloatArrayConverter.new(:array, :floats) float_converter.(array, strict: strict) end end class StringToNumericArrayConverter < Converter # Convert string value to array with numeric values # # @example # converter.call("1,2.0,3") # => [1, 2.0, 3] # # @api public def call(string, strict: config.strict) array_converter = StringToArrayConverter.new(:string, :array) array = array_converter.(string, strict: strict) num_converter = ArrayToNumericArrayConverter.new(:array, :numeric) num_converter.(array, strict: strict) end end class ArrayToIntegerArrayConverter < Converter # @example # converter.call(["1", "2", "3"]) # => [1, 2, 3] # # @api public def call(array, strict: config.strict) int_converter = NumericConverters::StringToIntegerConverter.new(:string, :integer) array.map { |val| int_converter.(val, strict: strict) } end end class ArrayToFloatArrayConverter < Converter # @example # converter.call(["1", "2", "3"]) # => [1.0, 2.0, 3.0] # # @api public def call(array, strict: config.strict) float_converter = NumericConverters::StringToFloatConverter.new(:string, :float) array.map { |val| float_converter.(val, strict: strict) } end end # An object that converts an array to an array with numeric values class ArrayToNumericArrayConverter < Converter # Convert an array to an array of numeric values # # @example # converter.call(["1", "2.3", "3.0]) # => [1, 2.3, 3.0] # # @param [Array] value # the value to convert # # @api public def call(value, strict: config.strict) num_converter = NumericConverters::StringToNumericConverter.new(:string, :numeric) value.map { |val| num_converter.(val, strict: strict) } end end # An object that converts an array to an array with boolean values class ArrayToBooleanArrayConverter < Converter # @example # converter.call(["t", "f", "yes", "no"]) # => [true, false, true, false] # # @param [Array] value # the array value to boolean # # @api public def call(value, strict: config.strict) bool_converter = BooleanConverters::StringToBooleanConverter.new(:string, :boolean) value.map { |val| bool_converter.(val, strict: strict) } end end # An object that converts an object to an array class ObjectToArrayConverter < Converter # Convert an object to an array # # @example # converter.call({x: 1}) # => [[:x, 1]] # # @api public def call(value, strict: config.strict) Array(value) rescue StandardError strict ? raise_conversion_type(value) : value end end # An object that converts an array to a set class ArrayToSetConverter < Converter # Convert an array to a set # # @example # converter.call([:x, :y, :x, 1, 2, 1]) # => # # @param [Array] value # the array to convert # # @api public def call(value, strict: config.strict) value.to_set rescue StandardError strict ? raise_conversion_type(value) : value end end def self.load(conversions) [ NullConverter.new(:array, :array), StringToArrayConverter.new(:string, :array), StringToBooleanArrayConverter.new(:string, :bools), StringToBooleanArrayConverter.new(:string, :booleans), StringToIntegerArrayConverter.new(:string, :integers), StringToIntegerArrayConverter.new(:string, :ints), StringToFloatArrayConverter.new(:string, :floats), StringToNumericArrayConverter.new(:string, :numerics), StringToNumericArrayConverter.new(:string, :nums), ArrayToNumericArrayConverter.new(:array, :numerics), ArrayToNumericArrayConverter.new(:array, :nums), ArrayToIntegerArrayConverter.new(:array, :integers), ArrayToIntegerArrayConverter.new(:array, :ints), ArrayToFloatArrayConverter.new(:array, :floats), ArrayToBooleanArrayConverter.new(:array, :booleans), ArrayToBooleanArrayConverter.new(:array, :bools), ObjectToArrayConverter.new(:object, :array), ObjectToArrayConverter.new(:hash, :array) ].each do |converter| conversions.register converter end end end # ArrayConverters end # Necromancer necromancer-0.7.0/lib/necromancer/converters/boolean.rb000066400000000000000000000047321377267052300232340ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../converter" require_relative "../null_converter" module Necromancer # Container for Boolean converter classes module BooleanConverters TRUE_MATCHER = /^(yes|y|on|t(rue)?|1)$/i.freeze FALSE_MATCHER = /^(no|n|off|f(alse)?|0)$/i.freeze # An object that converts a String to a Boolean class StringToBooleanConverter < Converter # Convert value to boolean type including range of strings # # @param [Object] value # # @example # converter.call("True") # => true # # other values converted to true are: # 1, t, T, TRUE, true, True, y, Y, YES, yes, Yes, on, ON # # @example # converter.call("False") # => false # # other values coerced to false are: # 0, f, F, FALSE, false, False, n, N, NO, no, No, off, OFF # # @api public def call(value, strict: config.strict) case value.to_s when TRUE_MATCHER then true when FALSE_MATCHER then false else strict ? raise_conversion_type(value) : value end end end # An object that converts an Integer to a Boolean class IntegerToBooleanConverter < Converter # Convert integer to boolean # # @example # converter.call(1) # => true # # @example # converter.call(0) # => false # # @api public def call(value, strict: config.strict) !value.zero? rescue StandardError strict ? raise_conversion_type(value) : value end end # An object that converts a Boolean to an Integer class BooleanToIntegerConverter < Converter # Convert boolean to integer # # @example # converter.call(true) # => 1 # # @example # converter.call(false) # => 0 # # @api public def call(value, strict: config.strict) if %w[TrueClass FalseClass].include?(value.class.name) value ? 1 : 0 else strict ? raise_conversion_type(value) : value end end end def self.load(conversions) [ StringToBooleanConverter.new(:string, :boolean), IntegerToBooleanConverter.new(:integer, :boolean), BooleanToIntegerConverter.new(:boolean, :integer), NullConverter.new(:boolean, :boolean) ].each do |converter| conversions.register converter end end end # BooleanConverters end # Necromancer necromancer-0.7.0/lib/necromancer/converters/date_time.rb000066400000000000000000000045541377267052300235520ustar00rootroot00000000000000# frozen_string_literal: true require "date" require "time" require_relative "../converter" require_relative "../null_converter" module Necromancer # Container for Date converter classes module DateTimeConverters # An object that converts a String to a Date class StringToDateConverter < Converter # Convert a string value to a Date # # @example # converter.call("1-1-2015") # => "2015-01-01" # converter.call("01/01/2015") # => "2015-01-01" # converter.call("2015-11-12") # => "2015-11-12" # converter.call("12/11/2015") # => "2015-11-12" # # @api public def call(value, strict: config.strict) Date.parse(value) rescue StandardError strict ? raise_conversion_type(value) : value end end # An object that converts a String to a DateTime class StringToDateTimeConverter < Converter # Convert a string value to a DateTime # # @example # converer.call("1-1-2015") # => "2015-01-01T00:00:00+00:00" # converer.call("1-1-2015 15:12:44") # => "2015-01-01T15:12:44+00:00" # # @api public def call(value, strict: config.strict) DateTime.parse(value) rescue StandardError strict ? raise_conversion_type(value) : value end end class StringToTimeConverter < Converter # Convert a String value to a Time value # # @param [String] value # the value to convert # # @example # converter.call("01-01-2015") # => 2015-01-01 00:00:00 +0100 # converter.call("01-01-2015 08:35") # => 2015-01-01 08:35:00 +0100 # converter.call("12:35") # => 2015-01-04 12:35:00 +0100 # # @api public def call(value, strict: config.strict) Time.parse(value) rescue StandardError strict ? raise_conversion_type(value) : value end end def self.load(conversions) [ StringToDateConverter.new(:string, :date), NullConverter.new(:date, :date), StringToDateTimeConverter.new(:string, :datetime), NullConverter.new(:datetime, :datetime), StringToTimeConverter.new(:string, :time), NullConverter.new(:time, :time) ].each do |converter| conversions.register converter end end end # DateTimeConverters end # Necromancer necromancer-0.7.0/lib/necromancer/converters/hash.rb000066400000000000000000000102511377267052300225310ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../converter" require_relative "boolean" require_relative "numeric" module Necromancer module HashConverters # An object that converts a String to a Hash class StringToHashConverter < Converter DEFAULT_CONVERSION = ->(val, *_rest) { val } # Convert string value to hash # # @example # converter.call("a:1 b:2 c:3") # # => {a: "1", b: "2", c: "3"} # # @example # converter.call("a=1 & b=3 & c=3") # # => {a: "1", b: "2", c: "3"} # # @api public def call(value, strict: config.strict, value_converter: DEFAULT_CONVERSION) values = value.split(/\s*[& ]\s*/) values.each_with_object({}) do |pair, pairs| key, value = pair.split(/[=:]/, 2) value_converted = value_converter.(value, strict: strict) if current = pairs[key.to_sym] pairs[key.to_sym] = Array(current) << value_converted else pairs[key.to_sym] = value_converted end pairs end end end class StringToIntegerHashConverter < Converter # Convert string value to hash with integer values # # @example # converter.call("a:1 b:2 c:3") # # => {a: 1, b: 2, c: 3} # # @api public def call(value, strict: config.strict) int_converter = NumericConverters::StringToIntegerConverter.new(:string, :integer) hash_converter = StringToHashConverter.new(:string, :hash) hash_converter.(value, strict: strict, value_converter: int_converter) end end class StringToFloatHashConverter < Converter # Convert string value to hash with float values # # @example # converter.call("a:1 b:2 c:3") # # => {a: 1.0, b: 2.0, c: 3.0} # # @api public def call(value, strict: config.strict) float_converter = NumericConverters::StringToFloatConverter.new(:string, :float) hash_converter = StringToHashConverter.new(:string, :hash) hash_converter.(value, strict: strict, value_converter: float_converter) end end class StringToNumericHashConverter < Converter # Convert string value to hash with numeric values # # @example # converter.call("a:1 b:2.0 c:3") # # => {a: 1, b: 2.0, c: 3} # # @api public def call(value, strict: config.strict) num_converter = NumericConverters::StringToNumericConverter.new(:string, :numeric) hash_converter = StringToHashConverter.new(:string, :hash) hash_converter.(value, strict: strict, value_converter: num_converter) end end class StringToBooleanHashConverter < Converter # Convert string value to hash with boolean values # # @example # converter.call("a:yes b:no c:t") # # => {a: true, b: false, c: true} # # @api public def call(value, strict: config.strict) bool_converter = BooleanConverters::StringToBooleanConverter.new(:string, :boolean) hash_converter = StringToHashConverter.new(:string, :hash) hash_converter.(value, strict: strict, value_converter: bool_converter) end end def self.load(conversions) [ NullConverter.new(:hash, :hash), StringToHashConverter.new(:string, :hash), StringToIntegerHashConverter.new(:string, :int_hash), StringToIntegerHashConverter.new(:string, :integer_hash), StringToFloatHashConverter.new(:string, :float_hash), StringToNumericHashConverter.new(:string, :num_hash), StringToNumericHashConverter.new(:string, :numeric_hash), StringToBooleanHashConverter.new(:string, :boolean_hash), StringToBooleanHashConverter.new(:string, :bool_hash) ].each do |converter| conversions.register(converter) end end end # HashConverters end # Necromancer necromancer-0.7.0/lib/necromancer/converters/numeric.rb000066400000000000000000000050301377267052300232470ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../converter" require_relative "../null_converter" module Necromancer # Container for Numeric converter classes module NumericConverters INTEGER_MATCHER = /^\s*[-+]?\s*(\d[\d\s]*)?$/.freeze FLOAT_MATCHER = /^\s*[-+]?([\d\s]*)(\.[\d\s]+)?([eE]?[-+]?[\d\s]+)?$/.freeze # An object that converts a String to an Integer class StringToIntegerConverter < Converter # Convert string value to integer # # @example # converter.call("1abc") # => 1 # # @api public def call(value, strict: config.strict) Integer(value) rescue StandardError strict ? raise_conversion_type(value) : value.to_i end end # An object that converts an Integer to a String class IntegerToStringConverter < Converter # Convert integer value to string # # @example # converter.call(1) # => "1" # # @api public def call(value, **_) value.to_s end end # An object that converts a String to a Float class StringToFloatConverter < Converter # Convert string to float value # # @example # converter.call("1.2") # => 1.2 # # @api public def call(value, strict: config.strict) Float(value) rescue StandardError strict ? raise_conversion_type(value) : value.to_f end end # An object that converts a String to a Numeric class StringToNumericConverter < Converter # Convert string to numeric value # # @example # converter.call("1.0") # => 1.0 # # @example # converter.call("1") # => 1 # # @api public def call(value, strict: config.strict) case value when INTEGER_MATCHER StringToIntegerConverter.new(:string, :integer).(value, strict: strict) when FLOAT_MATCHER StringToFloatConverter.new(:string, :float).(value, strict: strict) else strict ? raise_conversion_type(value) : value end end end def self.load(conversions) [ StringToIntegerConverter.new(:string, :integer), IntegerToStringConverter.new(:integer, :string), NullConverter.new(:integer, :integer), StringToFloatConverter.new(:string, :float), NullConverter.new(:float, :float), StringToNumericConverter.new(:string, :numeric) ].each do |converter| conversions.register converter end end end # Conversion end # Necromancer necromancer-0.7.0/lib/necromancer/converters/range.rb000066400000000000000000000040241377267052300227030ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../converter" require_relative "../null_converter" module Necromancer # Container for Range converter classes module RangeConverters SINGLE_DIGIT_MATCHER = /^(?-?\d+(\.\d+)?)$/.freeze DIGIT_MATCHER = /^(?-?\d+(\.\d+)?) \s*(?(\.\s*){2,3}|-|,)\s* (?-?\d+(\.\d+)?)$ /x.freeze LETTER_MATCHER = /^(?\w) \s*(?(\.\s*){2,3}|-|,)\s* (?\w)$ /x.freeze # An object that converts a String to a Range class StringToRangeConverter < Converter # Convert value to Range type with possible ranges # # @param [Object] value # # @example # converter.call("0,9") # => (0..9) # # @example # converter.call("0-9") # => (0..9) # # @api public def call(value, strict: config.strict) if match = value.match(SINGLE_DIGIT_MATCHER) digit = cast_to_num(match[:digit]) ::Range.new(digit, digit) elsif match = value.match(DIGIT_MATCHER) open = cast_to_num(match[:open]) close = cast_to_num(match[:close]) ::Range.new(open, close, match[:sep].gsub(/\s*/, "") == "...") elsif match = value.match(LETTER_MATCHER) ::Range.new(match[:open], match[:close], match[:sep].gsub(/\s*/, "") == "...") else strict ? raise_conversion_type(value) : value end end # Convert range end to numeric value # # @api private def cast_to_num(str) Integer(str) rescue ArgumentError Float(str) rescue ArgumentError nil end end def self.load(conversions) [ StringToRangeConverter.new(:string, :range), NullConverter.new(:range, :range) ].each do |converter| conversions.register converter end end end # RangeConverters end # Necromancer necromancer-0.7.0/lib/necromancer/null_converter.rb000066400000000000000000000003661377267052300224630ustar00rootroot00000000000000# frozen_string_literal: true require_relative "converter" module Necromancer # A pass through converter class NullConverter < Converter def call(value, strict: config.strict) value end end # NullConverter end # Necromancer necromancer-0.7.0/lib/necromancer/version.rb000066400000000000000000000001301377267052300210740ustar00rootroot00000000000000# frozen_string_literal: true module Necromancer VERSION = "0.7.0" end # Necromancer necromancer-0.7.0/necromancer.gemspec000066400000000000000000000024001377267052300176630ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/necromancer/version" Gem::Specification.new do |spec| spec.name = "necromancer" spec.version = Necromancer::VERSION spec.authors = ["Piotr Murach"] spec.email = ["piotr@piotrmurach.com"] spec.summary = %q{Conversion from one object type to another with a bit of black magic.} spec.description = %q{Conversion from one object type to another with a bit of black magic.} spec.homepage = "https://github.com/piotrmurach/necromancer" spec.license = "MIT" if spec.respond_to?(:metadata=) spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["changelog_uri"] = "https://github.com/piotrmurach/necromancer/blob/master/CHANGELOG.md" spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/necromancer" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/piotrmurach/necromancer" end spec.files = Dir["lib/**/*"] spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE.txt"] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.0.0" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", ">= 3.0" end necromancer-0.7.0/spec/000077500000000000000000000000001377267052300147605ustar00rootroot00000000000000necromancer-0.7.0/spec/spec_helper.rb000066400000000000000000000020711377267052300175760ustar00rootroot00000000000000# frozen_string_literal: true if ENV["COVERAGE"] == "true" require "simplecov" require "coveralls" SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ]) SimpleCov.start do command_name "spec" add_filter "spec" end end require "necromancer" RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.max_formatted_output_length = nil end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end # Limits the available syntax to the non-monkey patched syntax that is recommended. config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true if config.files_to_run.one? config.default_formatter = "doc" end config.profile_examples = 2 config.order = :random Kernel.srand config.seed end necromancer-0.7.0/spec/unit/000077500000000000000000000000001377267052300157375ustar00rootroot00000000000000necromancer-0.7.0/spec/unit/can_spec.rb000066400000000000000000000004241377267052300200370ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, "can?" do it "checks if conversion is possible" do converter = described_class.new expect(converter.can?(:string, :integer)).to eq(true) expect(converter.can?(:unknown, :integer)).to eq(false) end end necromancer-0.7.0/spec/unit/config_spec.rb000066400000000000000000000014431377267052300205450ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, "config" do it "configures global settings per instance" do converter = described_class.new converter.configure do |config| config.strict false end expect(converter.convert("1.2.3").to(:array)).to eq(["1.2.3"]) converter.configure do |config| config.strict true end expect { converter.convert("1.2.3").to(:array) }.to raise_error(Necromancer::ConversionTypeError) end it "configures global settings through instance block" do converter = described_class.new do |config| config.strict true end expect(converter.configuration.strict).to eq(true) expect { converter.convert("1.2.3").to(:array) }.to raise_error(Necromancer::ConversionTypeError) end end necromancer-0.7.0/spec/unit/configuration/000077500000000000000000000000001377267052300206065ustar00rootroot00000000000000necromancer-0.7.0/spec/unit/configuration/new_spec.rb000066400000000000000000000011551377267052300227400ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::Configuration, ".new" do subject(:config) { described_class.new } it { is_expected.to respond_to(:strict=) } it { is_expected.to respond_to(:copy=) } it "is in non-strict mode by default" do expect(config.strict).to eq(false) end it "is in copy mode by default" do expect(config.copy).to eq(true) end it "allows to set strict through method" do config.strict true expect(config.strict).to eq(true) end it "allows to set copy mode through method" do config.copy false expect(config.strict).to eq(false) end end necromancer-0.7.0/spec/unit/conversions/000077500000000000000000000000001377267052300203075ustar00rootroot00000000000000necromancer-0.7.0/spec/unit/conversions/fetch_spec.rb000066400000000000000000000010021377267052300227300ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::Conversions, "#fetch" do it "retrieves conversion given source & target" do converter = double(:converter) conversions = described_class.new nil, { "string->array" => converter } expect(conversions["string", "array"]).to eq(converter) end it "fails to find conversion" do conversions = described_class.new expect { conversions["string", "array"] }.to raise_error(Necromancer::NoTypeConversionAvailableError) end end necromancer-0.7.0/spec/unit/conversions/register_spec.rb000066400000000000000000000035771377267052300235060ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::Conversions, ".register" do it "allows to register converter" do context = described_class.new converter = double(:converter, { source: :string, target: :numeric }) expect(context.register(converter)).to eq(true) expect(context[:string, :numeric]).to eq(converter) end it "allows to register converter with no source" do context = described_class.new converter = double(:converter, { source: nil, target: :numeric }) expect(context.register(converter)).to eq(true) expect(context[:none, :numeric]).to eq(converter) end it "allows to register converter with no target" do context = described_class.new converter = double(:converter, { source: :string, target: nil }) expect(context.register(converter)).to eq(true) expect(context[:string, :none]).to eq(converter) end it "allows to register anonymous converter" do conversions = described_class.new conversions.register do |c| c.source = :string c.target = :upcase c.convert = ->(value) { value.to_s.upcase } end expect(conversions[:string, :upcase].("magic")).to eq("MAGIC") end it "allows to register anonymous converter with class names" do conversions = described_class.new conversions.register do |c| c.source = String c.target = Array c.convert = ->(value) { Array(value) } end expect(conversions[String, Array].("magic")).to eq(["magic"]) end it "allows to register custom converter" do conversions = described_class.new stub_const("UpcaseConverter", Struct.new(:source, :target) do def call(value) value.to_s.upcase end end) upcase_converter = UpcaseConverter.new(:string, :upcase) expect(conversions.register(upcase_converter)).to be(true) expect(conversions[:string, :upcase].("magic")).to eq("MAGIC") end end necromancer-0.7.0/spec/unit/conversions/to_hash_spec.rb000066400000000000000000000025711377267052300233000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::Conversions, "#to_hash" do it "exports default conversions to hash" do conversions = Necromancer::Conversions.new expect(conversions.to_hash).to eq({}) conversions.load expect(conversions.to_hash.keys.sort).to eq([ "array->array", "array->booleans", "array->bools", "array->floats", "array->integers", "array->ints", "array->numerics", "array->nums", "boolean->boolean", "boolean->integer", "date->date", "datetime->datetime", "float->float", "hash->array", "hash->hash", "integer->boolean", "integer->integer", "integer->string", "object->array", "range->range", "string->array", "string->bool_hash", "string->boolean", "string->boolean_hash", "string->booleans", "string->bools", "string->date", "string->datetime", "string->float", "string->float_hash", "string->floats", "string->hash", "string->int_hash", "string->integer", "string->integer_hash", "string->integers", "string->ints", "string->num_hash", "string->numeric", "string->numeric_hash", "string->numerics", "string->nums", "string->range", "string->time", "time->time" ]) end end necromancer-0.7.0/spec/unit/convert_spec.rb000066400000000000000000000133071377267052300207620ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, ".convert" do subject(:converter) { described_class.new } it "indicates inability to perform the requested conversion" do expect { converter.convert(:foo).to(:float) }.to raise_error(Necromancer::NoTypeConversionAvailableError, /Conversion 'symbol->float' unavailable/) end it "allows for module level convert call" do expect(Necromancer.convert("1,2,3").to(:array)).to eq(%w[1 2 3]) end it "allows replacing #to with #>> call" do expect(converter.convert("1,2,3") >> :integers).to eq([1, 2, 3]) end it "allows to specify object as conversion target" do expect(converter.convert("1,2,3") >> []).to eq(%w[1 2 3]) end it "allows to specify class as conversion target" do expect(converter.convert("1,2,3") >> Array).to eq(%w[1 2 3]) end context "when array" do it "converts string to array" do expect(converter.convert("1,2,3").to(:array)).to eq(%w[1 2 3]) end it "converts string to array of booleans" do expect(converter.convert("t,f,t").to(:booleans)).to eq([true, false, true]) expect(converter.convert("t,f,t").to(:bools)).to eq([true, false, true]) end it "converts string to array of integers" do expect(converter.convert("1,2,3").to(:integers)).to eq([1, 2, 3]) expect(converter.convert("1,2,3").to(:ints)).to eq([1, 2, 3]) end it "converts string to array of numerics" do expect(converter.convert("1,2.0,3").to(:numerics)).to eq([1, 2.0, 3]) expect(converter.convert("1,2.0,3").to(:nums)).to eq([1, 2.0, 3]) end it "converts array to numerics " do conversion = converter.convert(["1", "2.3", "3.0"]).to(:numerics) expect(conversion).to eq([1, 2.3, 3.0]) conversion = converter.convert(["1", "2.3", "3.0"]).to(:nums) expect(conversion).to eq([1, 2.3, 3.0]) end it "converts array to array of booleans" do expect(converter.convert(%w[t no]).to(:booleans)).to eq([true, false]) expect(converter.convert(%w[t no]).to(:bools)).to eq([true, false]) end it "converts object to array" do expect(converter.convert({ x: 1 }).to(:array)).to eq([[:x, 1]]) end it "fails to convert in strict mode" do expect { converter.convert(["1", "2.3", false]).to(:numerics, strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end context "when hash" do it "converts hash to hash" do conversion = converter.convert({ a: 1, b: 2, c: 3 }).to(:hash) expect(conversion).to eq({ a: 1, b: 2, c: 3 }) end it "converts string to hash" do conversion = converter.convert("a:1 b:2 c:3").to(:hash) expect(conversion).to eq({ a: "1", b: "2", c: "3" }) end it "converts string to integer hash" do conversion = converter.convert("a:1 b:2 c:3").to(:int_hash) expect(conversion).to eq({ a: 1, b: 2, c: 3 }) end it "converts string to float hash" do conversion = converter.convert("a:1 b:2 c:3").to(:float_hash) expect(conversion).to eq({ a: 1.0, b: 2.0, c: 3.0 }) end it "converts string to numeric hash" do conversion = converter.convert("a:1 b:2.0 c:3").to(:numeric_hash) expect(conversion).to eq({ a: 1, b: 2.0, c: 3 }) end it "converts string to boolean hash" do conversion = converter.convert("a:t b:f c:t").to(:boolean_hash) expect(conversion).to eq({ a: true, b: false, c: true }) end it "converts string to bool hash" do conversion = converter.convert("a:t b:f c:t").to(:bool_hash) expect(conversion).to eq({ a: true, b: false, c: true }) end end context "when numeric" do it "converts string to integer" do expect(converter.convert("1").to(:integer)).to eq(1) end it "allows for block for conversion method" do expect(converter.convert { "1" }.to(:integer)).to eq(1) end it "convers integer to string" do expect(converter.convert(1).to(:string)).to eq("1") end it "allows for null type conversion" do expect(converter.convert(1).to(:integer)).to eq(1) end it "raises error when in strict mode" do expect { converter.convert("1a").to(:integer, strict: true) }.to raise_error(Necromancer::ConversionTypeError) end it "doesn't raise error when in non-strict mode" do expect(converter.convert("1").to(:integer, strict: false)).to eql(1) end it "converts string to float" do expect(converter.convert("1.0").to(:float)).to eql(1.0) end it "converts string to numeric" do expect(converter.convert("1.0").to(:numeric)).to eql(1.0) end end context "when boolean" do it "converts boolean to boolean" do expect(converter.convert(true).to(:boolean)).to eq(true) end it "converts string to boolean" do expect(converter.convert("yes").to(:boolean)).to eq(true) end it "converts integer to boolean" do expect(converter.convert(0).to(:boolean)).to eq(false) end it "converts boolean to integer" do expect(converter.convert(true).to(:integer)).to eq(1) end end context "when range" do it "converts string to range" do expect(converter.convert("1-10").to(:range)).to eq(1..10) end end context "when datetime" do it "converts string to date" do expect(converter.convert("2014-12-07").to(:date)) .to eq(Date.parse("2014-12-07")) end it "converts string to datetime" do expect(converter.convert("2014-12-07 17:35:44").to(:datetime)) .to eq(DateTime.parse("2014-12-07 17:35:44")) end it "converts string to time" do expect(converter.convert("12:30").to(:time)) .to eq(Time.parse("12:30")) end end end necromancer-0.7.0/spec/unit/converters/000077500000000000000000000000001377267052300201315ustar00rootroot00000000000000necromancer-0.7.0/spec/unit/converters/array_spec.rb000066400000000000000000000157551377267052300226230ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::ArrayConverters, "#call" do describe ":string -> :array" do subject(:converter) { described_class::StringToArrayConverter.new(:string, :array) } { "" => [], "123" => %w[123], "1,2,3" => %w[1 2 3], "1-2-3" => %w[1 2 3], " 1 - 2 - 3 " => [" 1 ", " 2 ", " 3 "], "1 , 2 , 3" => ["1 ", " 2 ", " 3"], "1.2,2.3,3.4" => %w[1.2 2.3 3.4], "a,b,c" => %w[a b c], "a-b-c" => %w[a b c], "aa a,b bb,c c c" => %w[aa\ a b\ bb c\ c\ c] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert empty string to array in strict mode" do expect { converter.("unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `string` into `array`" ) end end describe ":string -> :booleans" do subject(:converter) { described_class::StringToBooleanArrayConverter.new } { "t,f,t" => [true, false, true], "yes,no,Y" => [true, false, true], "1-0-FALSE" => [true, false, false] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("yes,unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `string` into `boolean`" ) end end describe ":string -> :integers/:ints" do subject(:converter) { described_class::StringToIntegerArrayConverter.new } { "1,2,3" => [1, 2, 3], "1.2, 2.3, 3.4" => [1, 2, 3] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("1,unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `string` into `integer`" ) end end describe ":string -> :floats" do subject(:converter) { described_class::StringToFloatArrayConverter.new } { "1,2,3" => [1.0, 2.0, 3.0], "1.2, 2.3, 3.4" => [1.2, 2.3, 3.4] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("1,unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `string` into `float`" ) end end describe ":string -> :numerics/:nums" do subject(:converter) { described_class::StringToNumericArrayConverter.new } { "1,2.0,3" => [1, 2.0, 3], "1.2, 2.3, 3.4" => [1.2, 2.3, 3.4] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("1,unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `string` into `numeric`" ) end end describe ":array -> :booleans/:bools" do subject(:converter) { described_class::ArrayToBooleanArrayConverter.new(:array, :boolean) } { %w[t f yes no] => [true, false, true, false], %w[t no 5] => [true, false, "5"] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert `['t', 'no', 5]` to boolean array in strict mode" do expect { converter.(["t", "no", 5], strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'5' could not be converted from `string` into `boolean`" ) end end describe ":array -> :integers" do subject(:converter) { described_class::ArrayToIntegerArrayConverter.new } { %w[1 2 3] => [1, 2, 3], %w[1.2 2.3 3.4] => [1, 2, 3] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert `['1','2.3']` to integer array in strict mode" do expect { converter.(["1", "2.3"], strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'2.3' could not be converted from `string` into `integer`" ) end end describe ":array -> :floats" do subject(:converter) { described_class::ArrayToFloatArrayConverter.new } { %w[1 2 3] => [1.0, 2.0, 3.0], %w[1.2 2.3 3.4] => [1.2, 2.3, 3.4] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert `['1','2.3',false]` to float array in strict mode" do expect { converter.(["1", "2.3", false], strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'false' could not be converted from `string` into `float`" ) end end describe ":array -> :numerics/:nums" do subject(:converter) { described_class::ArrayToNumericArrayConverter.new(:array, :numerics) } { %w[1 2.3 3.0] => [1, 2.3, 3.0], %w[1 2.3 false] => [1, 2.3, "false"] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert `['1','2.3',false]` to numeric array in strict mode" do expect { converter.(["1", "2.3", false], strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'false' could not be converted from `string` into `numeric`" ) end end describe ":array -> :set" do subject(:converter) { described_class::ArrayToSetConverter.new(:array, :set) } it "converts `[:x,:y,:x,1,2,1]` to set" do expect(converter.([:x, :y, :x, 1, 2, 1])).to eql(Set[:x, :y, 1, 2]) end it "fails to convert `1` to set" do expect { converter.(1, strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end describe ":object -> :array" do subject(:converter) { described_class::ObjectToArrayConverter.new(:object, :array) } it "converts nil to array" do expect(converter.(nil)).to eq([]) end it "converts custom object to array" do stub_const("Custom", Class.new do def to_ary %i[x y] end end) custom = Custom.new expect(converter.(custom)).to eq(%i[x y]) end end end necromancer-0.7.0/spec/unit/converters/boolean_spec.rb000066400000000000000000000041761377267052300231170ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::BooleanConverters, "#call" do describe ":string -> :boolean" do subject(:converter) { described_class::StringToBooleanConverter.new(:string, :boolean) } it "passes through boolean value" do expect(converter.(true)).to eq(true) end %w[true TRUE t T 1 y Y YES yes on ON].each do |value| it "converts '#{value}' to true value" do expect(converter.(value)).to eq(true) end end %w[false FALSE f F 0 n N NO No no off OFF].each do |value| it "converts '#{value}' to false value" do expect(converter.(value)).to eq(false) end end it "raises error for empty string strict mode" do expect { converter.("", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end it "fails to convert unkonwn value FOO" do expect { converter.("FOO", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end describe ":boolean -> :integer" do subject(:converter) { described_class::BooleanToIntegerConverter.new(:boolean, :integer) } { true => 1, false => 0, "unknown" => "unknown" }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("unknown", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'unknown' could not be converted from `boolean` into `integer`" ) end end describe ":integer -> :boolean" do subject(:converter) { described_class::IntegerToBooleanConverter.new } { 1 => true, 0 => false, "unknown" => "unknown" }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert in strict mode" do expect { converter.("1", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end end necromancer-0.7.0/spec/unit/converters/date_time_spec.rb000066400000000000000000000044111377267052300234230ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::DateTimeConverters, "#call" do describe ":string -> :date" do subject(:converter) { described_class::StringToDateConverter.new(:string, :date) } { "" => "", "1-1-2015" => Date.parse("2015/01/01"), "2014/12/07" => Date.parse("2014/12/07"), "2014-12-07" => Date.parse("2014/12/07") }.each do |actual, expected| it "converts #{actual.inspect} to range type" do expect(converter.(actual)).to eql(expected) end end it "fails to convert in strict mode" do expect { converter.("2014 - 12 - 07", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'2014 - 12 - 07' could not be converted from `string` into `date`" ) end end describe ":string -> :datetime" do subject(:converter) { described_class::StringToDateTimeConverter.new(:string, :datetime) } { "" => "", "2014/12/07" => DateTime.parse("2014/12/07"), "2014-12-07" => DateTime.parse("2014-12-07"), "7th December 2014" => DateTime.parse("2014-12-07"), "7th December 2014 17:19:44" => DateTime.parse("2014-12-07 17:19:44") }.each do |actual, expected| it "converts #{actual.inspect} to range type" do expect(converter.(actual)).to eql(expected) end end it "fails to convert in strict mode" do expect { converter.("2014 - 12 - 07", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end describe ":string -> :time" do subject(:converter) { described_class::StringToTimeConverter.new(:string, :time) } { "" => "", "01/01/2015" => Time.parse("01/01/2015"), "01/01/2015 08:35" => Time.parse("01/01/2015 08:35"), "12:35" => Time.parse("12:35") }.each do |actual, expected| it "converts #{actual.inspect} to range type" do expect(converter.(actual)).to eql(expected) end end it "fails to convert in strict mode" do expect { converter.("11-13-2015", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end end necromancer-0.7.0/spec/unit/converters/hash_spec.rb000066400000000000000000000062601377267052300224170ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::HashConverters, "#call" do describe ":string -> :hash" do subject(:converter) { described_class::StringToHashConverter.new } { "a=1" => { a: "1" }, "a=1&b=2" => { a: "1", b: "2" }, "a= & b=2" => { a: "", b: "2" }, "a=1 & b=2 & a=3" => { a: %w[1 3], b: "2" }, "a:1 b:2" => { a: "1", b: "2" }, "a:1 b:2 a:3" => { a: %w[1 3], b: "2" } }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end end describe ":string -> :int_hash" do subject(:converter) { described_class::StringToIntegerHashConverter.new } { "a=1 & b=2 & a=3" => { a: [1, 3], b: 2 }, "a:1 b:2 c:3" => { a: 1, b: 2, c: 3 }, }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eq(obj) end end it "fails to convert '1.2o' value in strict mode" do expect { converter.("a=1.2o", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'1.2o' could not be converted from `string` into `integer`" ) end end describe ":string -> :float_hash" do subject(:converter) { described_class::StringToFloatHashConverter.new } { "a=1 & b=2 & a=3" => { a: [1.0, 3.0], b: 2.0 }, "a:1 b:2 c:3" => { a: 1.0, b: 2.0, c: 3.0 } }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eql(obj) end end it "fails to convert '1.2o' value in strict mode" do expect { converter.("a=1.2o", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'1.2o' could not be converted from `string` into `float`" ) end end describe ":string -> :numeric_hash" do subject(:converter) { described_class::StringToNumericHashConverter.new } { "a=1 & b=2.0 & a=3.0" => { a: [1, 3.0], b: 2.0 }, "a:1 b:2.0 c:3.0" => { a: 1, b: 2.0, c: 3.0 } }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eql(obj) end end it "fails to convert '1.2o' value in strict mode" do expect { converter.("a=1.2o", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'1.2o' could not be converted from `string` into `numeric`" ) end end describe ":string -> :bool_hash" do subject(:converter) { described_class::StringToBooleanHashConverter.new } { "a=t & b=t & a=f" => { a: [true, false], b: true }, "a:yes b:no c:t" => { a: true, b: false, c: true } }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(converter.(input)).to eql(obj) end end it "fails to convert '1.2o' value in strict mode" do expect { converter.("a=1.2", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'1.2' could not be converted from `string` into `boolean`" ) end end end necromancer-0.7.0/spec/unit/converters/numeric_spec.rb000066400000000000000000000071231377267052300231350ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::NumericConverters, "#call" do describe ":string -> :float" do subject(:converter) { described_class::StringToFloatConverter.new(:string, :float) } { "" => 0.0, "1" => 1.0, "+1" => 1.0, "1.2a" => 1.2, "-1" => -1.0, "1e1" => 10.0, "1e-1" => 0.1, "-1e1" => -10.0, "-1e-1" => -0.1, "1.0" => 1.0, "1.0e+1" => 10.0, "1.0e-1" => 0.1, "-1.0e+1" => -10.0, "-1.0e-1" => -0.1, ".1" => 0.1, ".1e+1" => 1.0, ".1e-1" => 0.01, "-.1e+1" => -1.0, "-.1e-1" => -0.01, " 1. 10 " => 1.0, " 1.0" => 1.0, " .1 " => 0.1, " -1.1 " => -1.1, " -1 . 1" => -1.0 }.each do |actual, expected| it "converts '#{actual}' to float value" do expect(converter.(actual)).to eql(expected) end end it "raises error for empty string in strict mode" do expect { converter.("", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end it "fails to convert '1.2a' in strict mode" do expect { converter.("1.2a", strict: true) }.to raise_error( Necromancer::ConversionTypeError, "'1.2a' could not be converted from `string` into `float`" ) end end describe ":string -> :integer" do subject(:converter) { described_class::StringToIntegerConverter.new(:string, :integer) } { "" => 0, "1" => 1, "1ab" => 1, "+1" => 1, "-1" => -1, "1e+1" => 1, "+1e-1" => 1, "-1e1" => -1, "-1e-1" => -1, "1.0" => 1, "1.0e+1" => 1, "1.0e-1" => 1, "-1.0e+1" => -1, "-1.0e-1" => -1, ".1" => 0, ".1e+1" => 0, ".1e-1" => 0, "-.1e+1" => 0, "-.1e-1" => 0, " 1 00" => 1, " 1 " => 1, " -1 " => -1 }.each do |actual, expected| it "converts '#{actual}' to float value" do expect(converter.(actual)).to eql(expected) end end it "raises error for empty string in strict mode" do expect { converter.("", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end it "raises error for float in strict mode" do expect { converter.("1.2", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end it "raises error for mixed string in strict mode" do expect { converter.("1abc", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end describe ":string -> :numeric" do subject(:converter) { described_class::StringToNumericConverter.new(:string, :numeric) } { "" => 0, "1" => 1, "+1" => 1, "-1" => -1, "1e1" => 10.0, "1e-1" => 0.1, "-1e1" => -10.0, "-1e-1" => -0.1, "1.0" => 1.0, "1.0e+1" => 10.0, "1.0e-1" => 0.1, "-1.0e+1" => -10.0, "-1.0e-1" => -0.1, ".1" => 0.1, ".1e+1" => 1.0, ".1e-1" => 0.01, "-.1e+1" => -1.0, "-.1e-1" => -0.01, " 1 00" => 1, " 1 " => 1, " -1 " => -1, " 1. 10 " => 1.0, " 1.0" => 1.0, " .1 " => 0.1, " -1.1 " => -1.1, " -1 . 1" => -1.0 }.each do |actual, expected| it "converts '#{actual}' to '#{expected}'" do expect(converter.(actual)).to eql(expected) end end end end necromancer-0.7.0/spec/unit/converters/range_spec.rb000066400000000000000000000017571377267052300225760ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer::RangeConverters, "#call" do describe ":string -> :range" do subject(:converter) { described_class::StringToRangeConverter.new } { "" => "", "a" => "a", "1" => 1..1, "1.0" => 1.0..1.0, "1..10" => 1..10, "1.0..10.0" => 1.0..10.0, "1-10" => 1..10, "1 , 10" => 1..10, "1...10" => 1...10, "1 . . 10" => 1..10, "-1..10" => -1..10, "1..-10" => 1..-10, "a..z" => "a".."z", "a . . . z" => "a"..."z", "a-z" => "a".."z", "A , Z" => "A".."Z" }.each do |actual, expected| it "converts #{actual.inspect} to range type" do expect(converter.(actual)).to eql(expected) end end it "raises error for empty string in strict mode" do expect { converter.("", strict: true) }.to raise_error(Necromancer::ConversionTypeError) end end end necromancer-0.7.0/spec/unit/inspect_spec.rb000066400000000000000000000010371377267052300207440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, ".inspect" do subject(:converter) { described_class.new } it "inspects converter instance" do expect(converter.inspect).to eq( "#" ) end it "inspects conversion target" do conversion = converter.convert(11) expect(conversion.inspect).to eq( "#" ) end end necromancer-0.7.0/spec/unit/new_spec.rb000066400000000000000000000003141377267052300200650ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, "#new" do subject(:converter) { described_class.new } it "creates context" do expect(converter).to be_a(Necromancer::Context) end end necromancer-0.7.0/spec/unit/register_spec.rb000066400000000000000000000007561377267052300211320ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Necromancer, ".register" do it "allows ro register converter" do converter = described_class.new stub_const("UpcaseConverter", Struct.new(:source, :target) do def call(value, **_options) value.to_s.upcase end end) upcase_converter = UpcaseConverter.new(:string, :upcase) expect(converter.register(upcase_converter)).to eq(true) expect(converter.convert("magic").to(:upcase)).to eq("MAGIC") end end necromancer-0.7.0/tasks/000077500000000000000000000000001377267052300151535ustar00rootroot00000000000000necromancer-0.7.0/tasks/console.rake000066400000000000000000000003241377267052300174600ustar00rootroot00000000000000# frozen_string_literal: true desc "Load gem inside irb console" task :console do require "irb" require "irb/completion" require_relative "../lib/necromancer" ARGV.clear IRB.start end task c: :console necromancer-0.7.0/tasks/coverage.rake000066400000000000000000000003361377267052300176140ustar00rootroot00000000000000# frozen_string_literal: true desc "Measure code coverage" task :coverage do begin original, ENV["COVERAGE"] = ENV["COVERAGE"], "true" Rake::Task["spec"].invoke ensure ENV["COVERAGE"] = original end end necromancer-0.7.0/tasks/spec.rake000066400000000000000000000012601377267052300167500ustar00rootroot00000000000000# frozen_string_literal: true begin require "rspec/core/rake_task" desc "Run all specs" RSpec::Core::RakeTask.new(:spec) do |task| task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb" end namespace :spec do desc "Run unit specs" RSpec::Core::RakeTask.new(:unit) do |task| task.pattern = "spec/unit{,/*/**}/*_spec.rb" end desc "Run integration specs" RSpec::Core::RakeTask.new(:integration) do |task| task.pattern = "spec/integration{,/*/**}/*_spec.rb" end end rescue LoadError %w[spec spec:unit spec:integration].each do |name| task name do warn "In order to run #{name}, do `gem install rspec`" end end end