lab42_data_class 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +99 -0
- data/lib/lab42/data_class/for_module.rb +15 -0
- data/lib/lab42/data_class/proxy.rb +104 -0
- data/lib/lab42/data_class/version.rb +8 -0
- data/lib/lab42/data_class.rb +13 -0
- metadata +48 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 03bada220d0a003c294685e37c3276b15786353f753b60cbafdaf6c78e3f986c
         | 
| 4 | 
            +
              data.tar.gz: d36b51077068701de6ecd9db90831d6290b2620d4d403402403ad4af3f1420b2
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 931c7c48bd60f8cfdc8ae711b292facf18622de31fbf7d04116af68a70c559171384a024ef628195789a9c25121db5c10879ffc36d99d81fb71cbcd88e25baca
         | 
| 7 | 
            +
              data.tar.gz: b2924660d7b0476092a9c85bd4d35c26fb59d4365d1a3f0ff2021343b336bdc28dd5a60dfd9fefb49a9f150cd0c955f71da0d896d72c17f37aa9a6f4352aff3f
         | 
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            Copyright © 2014 Dave Thomas, The Pragmatic Programmers
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              Licensed under the Apache License, Version 2.0 (the "License");
         | 
| 4 | 
            +
              you may not use this file except in compliance with the License.
         | 
| 5 | 
            +
              You may obtain a copy of the License at
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  https://spdx.org/licenses/Apache-2.0.html
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              Unless required by applicable law or agreed to in writing, software
         | 
| 10 | 
            +
              distributed under the License is distributed on an "AS IS" BASIS,
         | 
| 11 | 
            +
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         | 
| 12 | 
            +
              See the License for the specific language governing permissions and
         | 
| 13 | 
            +
              limitations under the License.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            Note:
         | 
| 16 | 
            +
            Individual files contain the following tag instead of the full license text.
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    SPDX-License-Identifier: Apache-2.0
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,99 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            [](https://rubygems.org/gems/lab42_data_class)
         | 
| 3 | 
            +
            [](https://github.com/robertdober/lab42_data_class/actions)
         | 
| 4 | 
            +
            [](https://coveralls.io/github/RobertDober/lab42_data_class?branch=main)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
             | 
| 7 | 
            +
            # Lab42::DataClass
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            A dataclass with an immutable API (you can still change the state of the object with metaprogramming and `lab42_immutable` is not ready yet!)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ## So what does it do?
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            Well let us [speculate about](https://github.com/RobertDober/speculate_about) it to find out:
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            ### Context: `DataClass` function
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Given
         | 
| 18 | 
            +
            ```ruby
         | 
| 19 | 
            +
                let(:my_data_class) { DataClass(:name, email: nil) }
         | 
| 20 | 
            +
                let(:my_instance) { my_data_class.new(name: "robert") }
         | 
| 21 | 
            +
            ```
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            Then we can access its fields
         | 
| 24 | 
            +
            ```ruby
         | 
| 25 | 
            +
                expect(my_instance.name).to eq("robert")
         | 
| 26 | 
            +
                expect(my_instance.email).to be_nil
         | 
| 27 | 
            +
            ```
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            But we cannot access undefined fields
         | 
| 30 | 
            +
            ```ruby
         | 
| 31 | 
            +
                expect{ my_instance.undefined }.to raise_error(NoMethodError)
         | 
| 32 | 
            +
            ```
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            And we need to provide values to fields without defaults
         | 
| 35 | 
            +
            ```ruby
         | 
| 36 | 
            +
                expect{ my_data_class.new(email: "some@mail.org") }
         | 
| 37 | 
            +
                  .to raise_error(ArgumentError, "missing initializers for [:name]")
         | 
| 38 | 
            +
            ```
         | 
| 39 | 
            +
            And we can extract the values
         | 
| 40 | 
            +
            ```ruby
         | 
| 41 | 
            +
                expect(my_instance.to_h).to eq(name: "robert", email: nil)
         | 
| 42 | 
            +
            ```
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            #### Context: Immutable
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            Given
         | 
| 47 | 
            +
            ```ruby
         | 
| 48 | 
            +
                let(:other_instance) { my_instance.merge(email: "robert@mail.provider") }
         | 
| 49 | 
            +
            ```
         | 
| 50 | 
            +
            Then we have a new instance with the old instance unchanged
         | 
| 51 | 
            +
            ```ruby
         | 
| 52 | 
            +
                expect(other_instance.to_h).to eq(name: "robert", email: "robert@mail.provider")
         | 
| 53 | 
            +
                expect(my_instance.to_h).to eq(name: "robert", email: nil)
         | 
| 54 | 
            +
            ```
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            ### Context: Defining behavior with blocks
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            Given
         | 
| 59 | 
            +
            ```ruby
         | 
| 60 | 
            +
                let :my_data_class do
         | 
| 61 | 
            +
                  DataClass :value, prefix: "<", suffix: ">" do
         | 
| 62 | 
            +
                    def show
         | 
| 63 | 
            +
                      [prefix, value, suffix].join
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
                let(:my_instance) { my_data_class.new(value: 42) }
         | 
| 68 | 
            +
            ```
         | 
| 69 | 
            +
             | 
| 70 | 
            +
            Then I have defined a method on my dataclass
         | 
| 71 | 
            +
            ```ruby
         | 
| 72 | 
            +
                expect(my_instance.show).to eq("<42>")
         | 
| 73 | 
            +
            ```
         | 
| 74 | 
            +
            ### Context: Making a dataclass from a class
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            Given
         | 
| 77 | 
            +
            ```ruby
         | 
| 78 | 
            +
                class DC
         | 
| 79 | 
            +
                  dataclass x: 1, y: 41
         | 
| 80 | 
            +
                  def sum; x + y end
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
            ```
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            Then we can define methods on it
         | 
| 85 | 
            +
            ```ruby
         | 
| 86 | 
            +
               expect(DC.new.sum).to eq(42)
         | 
| 87 | 
            +
            ```
         | 
| 88 | 
            +
             | 
| 89 | 
            +
            And we have a nice name for our instances
         | 
| 90 | 
            +
            ```ruby
         | 
| 91 | 
            +
                expect(DC.new.class.name).to eq("DC")
         | 
| 92 | 
            +
            ```
         | 
| 93 | 
            +
             | 
| 94 | 
            +
            # LICENSE
         | 
| 95 | 
            +
             | 
| 96 | 
            +
            Copyright 2022 Robert Dober robert.dober@gmail.com
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            Apache-2.0 [c.f LICENSE](LICENSE)
         | 
| 99 | 
            +
            <!-- SPDX-License-Identifier: Apache-2.0-->
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'proxy'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Lab42
         | 
| 6 | 
            +
              module DataClass
         | 
| 7 | 
            +
                class ::Module
         | 
| 8 | 
            +
                  def dataclass(*args, **defaults)
         | 
| 9 | 
            +
                    proxy = Lab42::DataClass::Proxy.new(*args, __klass__: self, **defaults)
         | 
| 10 | 
            +
                    proxy.define_class!
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
            end
         | 
| 15 | 
            +
            #  SPDX-License-Identifier: Apache-2.0
         | 
| @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Lab42
         | 
| 4 | 
            +
              module DataClass
         | 
| 5 | 
            +
                class Proxy
         | 
| 6 | 
            +
                  attr_reader :actual_params, :block, :defaults, :klass, :has_parent, :members, :positionals
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def check!(**params)
         | 
| 9 | 
            +
                    @actual_params = params
         | 
| 10 | 
            +
                    raise ArgumentError, "missing initializers for #{_missing_initializers}" unless _missing_initializers.empty?
         | 
| 11 | 
            +
                    raise ArgumentError, "illegal initializers #{_illegal_initializers}" unless _illegal_initializers.empty?
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def define_class!
         | 
| 15 | 
            +
                    klass.module_eval(&_define_attr_reader)
         | 
| 16 | 
            +
                    klass.module_eval(&_define_initializer)
         | 
| 17 | 
            +
                    _define_methods
         | 
| 18 | 
            +
                    klass
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def init(data_class, **params)
         | 
| 22 | 
            +
                    _init(data_class, defaults.merge(params))
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def to_hash(data_class_instance)
         | 
| 26 | 
            +
                    members
         | 
| 27 | 
            +
                      .map { [_1, data_class_instance.instance_variable_get("@#{_1}")] }
         | 
| 28 | 
            +
                      .to_h
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  private
         | 
| 32 | 
            +
                  def initialize(*args, **kwds, &blk)
         | 
| 33 | 
            +
                    @klass = kwds.fetch(:__klass__){ Class.new }
         | 
| 34 | 
            +
                    @has_parent = !!kwds.delete(:__klass__)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    @block = blk
         | 
| 37 | 
            +
                    @defaults = kwds
         | 
| 38 | 
            +
                    @members = Set.new(args + kwds.keys)
         | 
| 39 | 
            +
                    # TODO: Check for all symbols and no duplicates ⇒ v0.1.1
         | 
| 40 | 
            +
                    @positionals = args
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def _define_attr_reader
         | 
| 44 | 
            +
                    proxy = self
         | 
| 45 | 
            +
                    ->(*) do
         | 
| 46 | 
            +
                      attr_reader(*proxy.members)
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def _define_initializer
         | 
| 51 | 
            +
                    proxy = self
         | 
| 52 | 
            +
                    ->(*) do
         | 
| 53 | 
            +
                      define_method :initialize do |**params|
         | 
| 54 | 
            +
                        proxy.check!(**params)
         | 
| 55 | 
            +
                        proxy.init(self, **params)
         | 
| 56 | 
            +
                      end
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def _define_merge
         | 
| 61 | 
            +
                    proxy = self
         | 
| 62 | 
            +
                    ->(*) do
         | 
| 63 | 
            +
                      define_method :merge do |**params|
         | 
| 64 | 
            +
                        values = to_h.merge(params)
         | 
| 65 | 
            +
                        DataClass(*proxy.positionals, __klass__: self.class, **proxy.defaults, &proxy.block)
         | 
| 66 | 
            +
                          .new(**values)
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def _define_methods
         | 
| 72 | 
            +
                    klass.module_eval(&_define_to_h)
         | 
| 73 | 
            +
                    klass.module_eval(&_define_merge)
         | 
| 74 | 
            +
                    klass.module_eval(&block) if block
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def _define_to_h
         | 
| 78 | 
            +
                    proxy = self
         | 
| 79 | 
            +
                    ->(*) do
         | 
| 80 | 
            +
                      define_method :to_h do
         | 
| 81 | 
            +
                        proxy.to_hash(self)
         | 
| 82 | 
            +
                      end
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def _init(data_class_instance, params)
         | 
| 87 | 
            +
                    params.each do |key, value|
         | 
| 88 | 
            +
                      data_class_instance.instance_variable_set("@#{key}", value)
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def _missing_initializers
         | 
| 93 | 
            +
                    @___missing_initializers__ ||=
         | 
| 94 | 
            +
                      positionals - actual_params.keys
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  def _illegal_initializers
         | 
| 98 | 
            +
                    @___illegal_initializers__ ||=
         | 
| 99 | 
            +
                      actual_params.keys - positionals - defaults.keys
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
            end
         | 
| 104 | 
            +
            #  SPDX-License-Identifier: Apache-2.0
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative './data_class/for_module'
         | 
| 4 | 
            +
            require_relative './data_class/proxy'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Kernel
         | 
| 7 | 
            +
              def DataClass(*args, **kwds, &blk)
         | 
| 8 | 
            +
                proxy = Lab42::DataClass::Proxy.new(*args, **kwds, &blk)
         | 
| 9 | 
            +
                proxy.define_class!
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
            end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            #  SPDX-License-Identifier: Apache-2.0
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: lab42_data_class
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Robert Dober
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2022-01-20 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies: []
         | 
| 13 | 
            +
            description: introduces a new Kernel function DataClass
         | 
| 14 | 
            +
            email: robert.dober@gmail.com
         | 
| 15 | 
            +
            executables: []
         | 
| 16 | 
            +
            extensions: []
         | 
| 17 | 
            +
            extra_rdoc_files: []
         | 
| 18 | 
            +
            files:
         | 
| 19 | 
            +
            - LICENSE
         | 
| 20 | 
            +
            - README.md
         | 
| 21 | 
            +
            - lib/lab42/data_class.rb
         | 
| 22 | 
            +
            - lib/lab42/data_class/for_module.rb
         | 
| 23 | 
            +
            - lib/lab42/data_class/proxy.rb
         | 
| 24 | 
            +
            - lib/lab42/data_class/version.rb
         | 
| 25 | 
            +
            homepage: https://github.com/robertdober/lab42_data_class
         | 
| 26 | 
            +
            licenses:
         | 
| 27 | 
            +
            - Apache-2.0
         | 
| 28 | 
            +
            metadata: {}
         | 
| 29 | 
            +
            post_install_message: 
         | 
| 30 | 
            +
            rdoc_options: []
         | 
| 31 | 
            +
            require_paths:
         | 
| 32 | 
            +
            - lib
         | 
| 33 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 34 | 
            +
              requirements:
         | 
| 35 | 
            +
              - - ">="
         | 
| 36 | 
            +
                - !ruby/object:Gem::Version
         | 
| 37 | 
            +
                  version: 3.1.0
         | 
| 38 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 39 | 
            +
              requirements:
         | 
| 40 | 
            +
              - - ">="
         | 
| 41 | 
            +
                - !ruby/object:Gem::Version
         | 
| 42 | 
            +
                  version: '0'
         | 
| 43 | 
            +
            requirements: []
         | 
| 44 | 
            +
            rubygems_version: 3.3.3
         | 
| 45 | 
            +
            signing_key: 
         | 
| 46 | 
            +
            specification_version: 4
         | 
| 47 | 
            +
            summary: Finally a dataclass in ruby
         | 
| 48 | 
            +
            test_files: []
         |