lite-data 0.0.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d7ccf91b7250931a7aece3d9395a803039536854c4cceb244f8937bd8804446
4
+ data.tar.gz: 9dd190fdeecad8b5f699089350ce5a9934767edbdb1146cbdc8ff714adce2c7c
5
+ SHA512:
6
+ metadata.gz: 789357cc7e35536f018b3bd61395d2ed513cca435f0bd3190e51f15e07692c732e4f571242c96f9ccbe571817fe90ce3a64c7a1483b1e17cb87eaddb1e2833bd
7
+ data.tar.gz: 8debe38ea903f30f960981bde64b8eaa5377934e717691314f042e331226770a4fd8e350ada9b0a0d5d1141432fabe97c0f96240c04fab32827139a242baa5d7
data/.rubocop.yml ADDED
@@ -0,0 +1,72 @@
1
+ plugins:
2
+ rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Style/DocumentDynamicEvalDefinition:
11
+ Enabled: false
12
+
13
+ Metrics/MethodLength:
14
+ Max: 20
15
+
16
+ Metrics/ModuleLength:
17
+ Max: 200
18
+
19
+ Metrics/BlockLength:
20
+ Exclude:
21
+ - spec/**/*.rb
22
+
23
+ Layout/LineLength:
24
+ Exclude:
25
+ - spec/**/*.rb
26
+
27
+ Lint/ConstantDefinitionInBlock:
28
+ Exclude:
29
+ - spec/**/*.rb
30
+
31
+ Lint/ShadowingOuterLocalVariable:
32
+ Enabled: false
33
+
34
+ Layout/EndAlignment:
35
+ EnforcedStyleAlignWith: start_of_line
36
+
37
+ Rspec/MultipleExpectations:
38
+ Max: 3
39
+
40
+ Style/TrailingUnderscoreVariable:
41
+ Enabled: false
42
+
43
+ Style/MultilineBlockChain:
44
+ Enabled: false
45
+
46
+ Style/EachWithObject:
47
+ Enabled: false
48
+
49
+ Style/HashSyntax:
50
+ Enabled: false
51
+
52
+ Lint/BooleanSymbol:
53
+ Enabled: false
54
+
55
+ Style/HashConversion:
56
+ Enabled: false
57
+
58
+ Style/RedundantArrayConstructor:
59
+ Enabled: false
60
+
61
+ Style/RedundantSelfAssignment:
62
+ Enabled: false
63
+
64
+ Style/MethodCallWithoutArgsParentheses:
65
+ Enabled: false
66
+
67
+ Style/ArgumentsForwarding:
68
+ Enabled: false
69
+
70
+ Naming/BlockForwarding:
71
+ Enabled: false
72
+
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ group :test do
6
+ gem 'byebug'
7
+ gem 'markly'
8
+ gem 'rspec'
9
+ gem 'rubocop'
10
+ gem 'rubocop-rspec'
11
+ gem 'simplecov'
12
+ end
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Lite::Data
2
+ Easy definition of data classes with subclassing support
3
+ and flexible constructor signatures.
4
+
5
+ ## Overview
6
+ Closely follows the interface of Ruby 3.2's `Data` class, but adds:
7
+ - Subclassing capability
8
+ - Mixed positional and keyword arguments
9
+ - Module-based design for including in existing classes
10
+
11
+ ## Class definition
12
+ ### Defining a data class
13
+ Use `Lite::Data.define(klass, args:, kwargs:)` with arrays for positional
14
+ and keyword parameters. The following declaration generates a module with
15
+ necessary instance methods and includes it in the target class:
16
+
17
+ ```ruby rspec definition_superclass
18
+ class Foo
19
+ Lite::Data.define(self, args: [:foo])
20
+ end
21
+
22
+ expect(Foo.new('FOO').foo).to eq('FOO')
23
+ ```
24
+
25
+ The same works when extending existing data classes:
26
+ ```ruby rspec definition_subclass
27
+ class Bar < Foo
28
+ Lite::Data.define(self, kwargs: [:bar])
29
+ end
30
+
31
+ expect(Bar.new('FOO', bar: 'BAR').bar).to eq('BAR')
32
+ ```
33
+
34
+ ### Argument positioning in subclasses
35
+ By default, new positional arguments are added at the start. Use `:'*'`
36
+ as a placeholder to control positioning:
37
+
38
+ ```ruby rspec definition_argument_positioning
39
+ class Bax < Bar
40
+ Lite::Data.define(self, args: [:'*', :bax])
41
+ end
42
+
43
+ expect(Bax.new('FOO', 'BAX', bar: 'BAR').bax).to eq('BAX')
44
+ ```
45
+
46
+ ## Generated methods
47
+ Classes automatically receive:
48
+ - **Comparison**: `==`, `eql?`, `hash`
49
+ - **Cloning**: `with`
50
+ - **Introspection**: `deconstruct`, `deconstruct_keys`, `to_h`
51
+
52
+ ### Destructuring order
53
+ The `deconstruct` method returns values in order of inheritance,
54
+ each class contributes its members in order of definition,
55
+ positional first:
56
+ ```ruby rspec introspection_deconstruct
57
+ # inheritance chain: Bax < Bar < Foo
58
+ expect(Bax.new('FOO', 'BAX', bar: 'BAR').deconstruct)
59
+ .to eq(%w[FOO BAR BAX])
60
+ ```
61
+
62
+ # License
63
+ This library is published under MIT license
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'byebug'
5
+
6
+ require_relative '../lib/lite/data'
7
+
8
+ module Lite
9
+ module Data
10
+ module Benchmark
11
+ module Comparative
12
+ module Abstract
13
+ def instantiate(opts)
14
+ self::Data.new(**opts)
15
+ end
16
+
17
+ def clone(original, opts)
18
+ original.with(**opts.compact)
19
+ end
20
+ end
21
+
22
+ module RubyBase
23
+ extend Abstract
24
+
25
+ Data = ::Data.define(:foo, :bar, :bax, :qox)
26
+ end
27
+
28
+ module LiteBase
29
+ extend Abstract
30
+
31
+ class Data
32
+ Lite::Data.define(self, kwargs: %i[foo bar bax qox])
33
+ end
34
+ end
35
+
36
+ module LiteSubclass
37
+ extend Abstract
38
+
39
+ class Super
40
+ Lite::Data.define(self, kwargs: %i[foo bar])
41
+ end
42
+
43
+ class Data < Super
44
+ Lite::Data.define(self, kwargs: %i[bax qox])
45
+ end
46
+ end
47
+
48
+ SYMBOLS = %i[a b c d e f g h i j].freeze
49
+
50
+ def self.run(n, duplicity: 0) # rubocop:disable Naming/MethodParameterName, Metrics/AbcSize
51
+ subjects = [RubyBase, LiteBase, LiteSubclass]
52
+
53
+ original_opts = opts
54
+ idata = 10_000.times.map { Random.rand < duplicity ? original_opts : opts }
55
+ cdata = idata.map(&:compact)
56
+
57
+ subjects.to_a.each do |subject|
58
+ result = ::Benchmark.measure do
59
+ n.times { |idx| subject.instantiate(idata[idx % idata.length]) }
60
+ end
61
+ puts "#{subject.name} NEW: #{result}"
62
+
63
+ original = subject.instantiate(original_opts)
64
+ result = ::Benchmark.measure do
65
+ n.times do |idx|
66
+ subject.clone(original, cdata[idx % cdata.length])
67
+ end
68
+ end
69
+ puts "#{subject.name} CLONE: #{result}"
70
+ end
71
+ end
72
+
73
+ def self.opts
74
+ { foo: rand, bar: rand, bax: rand, qox: rand }
75
+ end
76
+
77
+ def self.rand
78
+ SYMBOLS[Random.rand(11)]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ Lite::Data::Benchmark::Comparative.run(100_000, duplicity: 0.1)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../marker'
4
+
5
+ module Lite
6
+ module Data
7
+ module Definer
8
+ module Abstract
9
+ def define(members)
10
+ Module.new do
11
+ extend Marker
12
+
13
+ attr_reader(*members.members)
14
+
15
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
16
+ def self.members
17
+ [#{members.members.map { ":#{_1}" }.join(', ')}]
18
+ end
19
+
20
+ def deconstruct
21
+ #{members.ivars_array}
22
+ end
23
+
24
+ def to_h
25
+ { #{members.hash_fields} }
26
+ end
27
+ RUBY
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+ require_relative 'members/base'
5
+
6
+ module Lite
7
+ module Data
8
+ module Definer
9
+ module Base
10
+ extend Abstract
11
+
12
+ module InstanceMethods
13
+ def deconstruct_keys(members)
14
+ case members
15
+ when nil then to_h
16
+ else to_h.slice(*members)
17
+ end
18
+ end
19
+
20
+ def inspect
21
+ "#<data #{self.class.name} #{to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}>"
22
+ end
23
+
24
+ def to_s
25
+ inspect
26
+ end
27
+ end
28
+
29
+ def self.define(positional_arguments, keyword_arguments) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
30
+ members = Members::Base.instance(positional_arguments, keyword_arguments)
31
+ mod = super(members)
32
+
33
+ mod.include InstanceMethods
34
+
35
+ mod.class_eval <<~RUBY, __FILE__, __LINE__ + 1
36
+ def initialize(#{members.initialize_signature})
37
+ #{members.initialize_ivars}
38
+ freeze
39
+ end
40
+
41
+ def ==(other)
42
+ return true if self.equal?(other)
43
+ return false unless self.class == other.class
44
+
45
+ #{members.equality}
46
+ end
47
+
48
+ def eql?(other)
49
+ return true if self.equal?(other)
50
+ return false unless self.class == other.class
51
+
52
+ #{members.hash_equality}
53
+ end
54
+
55
+ def hash
56
+ [#{['self.class', *members.members].join(', ')}].hash
57
+ end
58
+
59
+ def with(#{members.keyword_signature_defaults})
60
+ return self if #{members.variables_equal_attributes}
61
+
62
+ self.class.send(#{[':new', *members.constructor_arguments].join(', ')})
63
+ end
64
+
65
+ private
66
+
67
+ def merged_constructor_arguments(#{members.merged_constructor_arguments_signature})
68
+ identical &&= #{members.variables_equal_attributes}
69
+ return identical if identical
70
+
71
+ [false, #{members.merged_constructor_arguments}]
72
+ end
73
+ RUBY
74
+
75
+ mod
76
+ end
77
+
78
+ def self.define_class_methods(base, mod)
79
+ base.define_singleton_method(:members) { mod.members }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../error'
4
+
5
+ module Lite
6
+ module Data
7
+ module Definer
8
+ module Members
9
+ class Abstract
10
+ def self.instance(positional_arguments, keyword_arguments, members)
11
+ ensure_members_valid!(members)
12
+ new positional_arguments, keyword_arguments, members
13
+ end
14
+
15
+ def self.ensure_members_valid!(members)
16
+ raise Error, 'Members must not be empty' if members.empty?
17
+
18
+ invalid = members.reject { _1.is_a?(Symbol) }
19
+ raise Error, "Array of symbols expected, got: #{invalid.map(&:inspect).join(', ')}" unless invalid.empty?
20
+
21
+ uniq = members.uniq
22
+ raise Error, 'Member names must be unique' unless uniq.length == members.length
23
+ end
24
+
25
+ def initialize(positional_arguments, keyword_arguments, members)
26
+ @positional_arguments = positional_arguments.freeze
27
+ @keyword_arguments = keyword_arguments.freeze
28
+ @members = members.freeze
29
+ freeze
30
+ end
31
+
32
+ attr_reader :positional_arguments, :keyword_arguments, :members
33
+
34
+ def initialize_ivars
35
+ members.map { "@#{_1} = #{_1}" }.join(';')
36
+ end
37
+
38
+ def variables_equal_attributes
39
+ members.map { "#{_1} == @#{_1}" }.join(' && ')
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module Lite
6
+ module Data
7
+ module Definer
8
+ module Members
9
+ class Base < Abstract
10
+ def self.instance(positional_arguments, keyword_arguments)
11
+ super(
12
+ positional_arguments,
13
+ keyword_arguments,
14
+ positional_arguments + keyword_arguments
15
+ )
16
+ end
17
+
18
+ def initialize_signature
19
+ (positional_arguments + keyword_arguments.map { "#{_1}:" }).join(', ')
20
+ end
21
+
22
+ def keyword_signature_defaults
23
+ members.map { "#{_1}: @#{_1}" }.join(', ')
24
+ end
25
+
26
+ def constructor_arguments
27
+ (positional_arguments + keyword_arguments.map { "#{_1}: #{_1}" }).join(', ')
28
+ end
29
+
30
+ def ivars_array
31
+ "[#{members.map { " @#{_1}" }.join(', ')}]"
32
+ end
33
+
34
+ def positional_arguments_array
35
+ "[#{positional_arguments.join(', ')}]"
36
+ end
37
+
38
+ def keyword_arguments_hash
39
+ "{ #{keyword_arguments.map { "#{_1}: #{_1}" }.join(', ')} }"
40
+ end
41
+
42
+ def merged_constructor_arguments_signature
43
+ ['identical', members.map { "#{_1}: @#{_1}" }].join(', ')
44
+ end
45
+
46
+ def merged_constructor_arguments
47
+ "#{positional_arguments_array}, #{keyword_arguments_hash}"
48
+ end
49
+
50
+ def hash_fields
51
+ members.map { "#{_1}: @#{_1}" }.join(', ')
52
+ end
53
+
54
+ def equality
55
+ members.map { "#{_1} == other.#{_1}" }.join(' && ')
56
+ end
57
+
58
+ def hash_equality
59
+ members.map { "#{_1}.eql?(other.#{_1})" }.join(' && ')
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+
5
+ module Lite
6
+ module Data
7
+ module Definer
8
+ module Members
9
+ class Subclass < Abstract
10
+ def self.instance(positional_arguments, keyword_arguments)
11
+ positional_arguments = [*positional_arguments, :*] unless positional_arguments.include? :*
12
+
13
+ super(
14
+ positional_arguments,
15
+ keyword_arguments,
16
+ (positional_arguments.reject { _1 == :* } + keyword_arguments)
17
+ )
18
+ end
19
+
20
+ attr_reader :positional_arguments, :keyword_arguments, :members
21
+
22
+ def interpolated_positional_arguments(replacement)
23
+ positional_arguments.map { _1 == :* ? replacement : _1 }
24
+ end
25
+
26
+ def initialize_signature
27
+ [
28
+ *interpolated_positional_arguments('*args'),
29
+ *keyword_arguments.map { "#{_1}:" },
30
+ '**opts'
31
+ ].join(', ')
32
+ end
33
+
34
+ def ivars_array
35
+ "[*super, #{members.map { "@#{_1}" }.join(', ')}]"
36
+ end
37
+
38
+ def merged_constructor_arguments_signature
39
+ ['identical', members.map { "#{_1}: @#{_1}" }, '**opts'].join(', ')
40
+ end
41
+
42
+ def merged_constructor_arguments
43
+ positional = interpolated_positional_arguments('*args').join(', ')
44
+ keyword = [*keyword_arguments.map { "#{_1}: #{_1}" }, '**opts'].join(', ')
45
+ "[#{positional}], { #{keyword} }"
46
+ end
47
+
48
+ def hash_fields
49
+ ['**super', *members.map { "#{_1}: @#{_1}" }].join(', ')
50
+ end
51
+
52
+ def equality
53
+ ['super', *members.map { "#{_1} == other.#{_1}" }].join(' && ')
54
+ end
55
+
56
+ def hash_equality
57
+ ['super', *members.map { "#{_1}.eql?(other.#{_1})" }].join(' && ')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+ require_relative 'members/subclass'
5
+
6
+ module Lite
7
+ module Data
8
+ module Definer
9
+ module Subclass
10
+ extend Abstract
11
+
12
+ module InstanceMethods
13
+ def with(**opts)
14
+ return self if opts.empty?
15
+
16
+ identical, args, opts = merged_constructor_arguments(true, **opts)
17
+ return self if identical
18
+
19
+ self.class.send(:new, *args, **opts)
20
+ end
21
+ end
22
+
23
+ def self.define(positional_arguments, keyword_arguments) # rubocop:disable Metrics/MethodLength
24
+ members = Members::Subclass.instance(positional_arguments, keyword_arguments)
25
+ mod = super(members)
26
+
27
+ mod.include InstanceMethods
28
+
29
+ mod.class_eval <<~RUBY, __FILE__, __LINE__ + 1
30
+ def initialize(#{members.initialize_signature})
31
+ #{members.initialize_ivars}
32
+ super *args, **opts
33
+ end
34
+
35
+ def ==(other)
36
+ #{members.equality}
37
+ end
38
+
39
+ def eql?(other)
40
+ #{members.hash_equality}
41
+ end
42
+
43
+ def hash
44
+ [#{['super', *members.members].join(', ')}].hash
45
+ end
46
+
47
+ private
48
+
49
+ def merged_constructor_arguments(#{members.merged_constructor_arguments_signature})
50
+ identical &&= #{members.variables_equal_attributes}
51
+
52
+ return true if identical && opts.empty?
53
+ identical, args, opts = super(identical, **opts)
54
+ return true if identical
55
+
56
+ [false, #{members.merged_constructor_arguments}]
57
+ end
58
+ RUBY
59
+
60
+ mod
61
+ end
62
+
63
+ def self.define_class_methods(base, mod)
64
+ base.define_singleton_method(:members) { super() + mod.members }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Data
5
+ class Error < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Data
5
+ module Marker
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Data
5
+ module Version
6
+ VERSION = '0.0.3'
7
+ end
8
+ end
9
+ end
data/lib/lite/data.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data/definer/base'
4
+ require_relative 'data/definer/subclass'
5
+
6
+ module Lite
7
+ module Data
8
+ private_constant :Definer
9
+
10
+ def self.define(base, args: [], kwargs: [])
11
+ supermod = base.ancestors.find { |klass| klass.is_a?(Marker) }
12
+ prevent_conflicts!(base, args, kwargs) if supermod
13
+
14
+ definer = supermod ? Definer::Subclass : Definer::Base
15
+ mod = definer.define(args, kwargs)
16
+ definer.define_class_methods(base, mod)
17
+ base.include(mod)
18
+ nil
19
+ end
20
+
21
+ def self.prevent_conflicts!(klass, args, kwargs)
22
+ duplicates = klass.members & (args + kwargs)
23
+ raise Error, "Members already declared: #{duplicates.map(&:inspect).join(', ')}" unless duplicates.empty?
24
+ end
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lite-data
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Tomas Milsimer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-09-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Easy definition of data classes with subclassing support
15
+ and flexible constructor signatures.
16
+ email:
17
+ - tomas.milsimer@protonmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rubocop.yml"
23
+ - Gemfile
24
+ - README.md
25
+ - bench/comparative.rb
26
+ - lib/lite/data.rb
27
+ - lib/lite/data/definer/abstract.rb
28
+ - lib/lite/data/definer/base.rb
29
+ - lib/lite/data/definer/members/abstract.rb
30
+ - lib/lite/data/definer/members/base.rb
31
+ - lib/lite/data/definer/members/subclass.rb
32
+ - lib/lite/data/definer/subclass.rb
33
+ - lib/lite/data/error.rb
34
+ - lib/lite/data/marker.rb
35
+ - lib/lite/data/version.rb
36
+ homepage: https://github.com/lame-impala/lite-data
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.0.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.4.10
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Minimalistic data-class definition
60
+ test_files: []