Skip to content
This repository has been archived by the owner on Jan 1, 2024. It is now read-only.

Commit

Permalink
Pure Ruby re-implementation of Struct
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyObtiva committed Jan 2, 2021
1 parent dfb1670 commit babdc6c
Show file tree
Hide file tree
Showing 4 changed files with 410 additions and 2 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Pure Struct
[![Gem Version](https://badge.fury.io/rb/pure-struct.svg)](http://badge.fury.io/rb/pure-struct)
[![rspec](https://github.com/AndyObtiva/pure-struct/workflows/rspec/badge.svg)](https://github.com/AndyObtiva/pure-struct/actions?query=workflow%3Arspec)
[![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/pure-struct/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/pure-struct?branch=master)
[![Maintainability](https://api.codeclimate.com/v1/badges/2659b419fd5f7d38e443/maintainability)](https://codeclimate.com/github/AndyObtiva/pure-struct/maintainability)

Pure [Ruby](https://www.ruby-lang.org/) re-implementation of [Struct](https://ruby-doc.org/core-2.7.0/Struct.html) to ensure cross-Ruby functionality where needed (e.g. [Opal](https://opalrb.com/))

It is useful when:
- There is a need for a Struct class that works consistently across esoteric implementations of [Ruby](https://www.ruby-lang.org/) like [Opal](https://opalrb.com/)
- There is a need for a Struct class that works consistently across esoteric implementations of [Ruby](https://www.ruby-lang.org/) like [Opal](https://opalrb.com/). This is useful when writing cross-[Ruby](https://www.ruby-lang.org/) apps like those of [Glimmer](https://github.com/AndyObtiva/glimmer) relying on [YASL](https://github.com/AndyObtiva/yasl) (Yet Another Serialization Library) in [Opal](https://opalrb.com/).
- There is a need to meta-program Struct's data
- There are no big performance requirements that demand native [Struct](https://ruby-doc.org/core-2.7.0/Struct.html)

Expand Down Expand Up @@ -34,7 +35,9 @@ Finally, require in [Ruby](https://www.ruby-lang.org/) code:
require 'pure-struct'
```

Optionally, you may code block the require statement by a specific [Ruby](https://www.ruby-lang.org/) engine like [Opal](https://opalrb.com/) (useful when writing cross-Ruby apps like those of [Glimmer](https://github.com/AndyObtiva/glimmer)):
Note that it removes the native `Struct` implementation first, aliasing as `NativeStruct` should you still need it, and then redefining `Struct` in pure [Ruby](https://www.ruby-lang.org/).

Optionally, you may code block the require statement by a specific [Ruby](https://www.ruby-lang.org/) engine like [Opal](https://opalrb.com/):

```
if RUBY_ENGINE == 'opal'
Expand Down
142 changes: 142 additions & 0 deletions lib/pure-struct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright (c) 2021 Andy maleh
#
# 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.

NativeStruct = Struct
Object.send(:remove_const, :Struct) if Object.constants.include?(:Struct)

# Optional re-implmentation of Struct in Pure Ruby (to get around JS issues in Opal Struct)
class Struct
class << self
CLASS_DEFINITION_FOR_ATTRIBUTES = lambda do |attributes, keyword_init|
lambda do |defined_class|
attributes.each do |attribute|
define_method(attribute) do
self[attribute]
end
define_method("#{attribute}=") do |value|
self[attribute] = value
end
end

define_method(:members) do
@members ||= attributes
end

def []=(attribute, value)
normalized_attribute = attribute.to_sym
raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
@member_values[normalized_attribute] = value
end

def [](attribute)
normalized_attribute = attribute.to_sym
raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
@member_values[normalized_attribute]
end

def each(&block)
to_a.each(&block)
end

def each_pair(&block)
@member_values.each_pair(&block)
end

def to_h
@member_values.clone
end

def to_a
@member_values.values
end

def size
members.size
end
alias length size

def dig(*args)
@member_values.dig(*args)
end

def select(&block)
to_a.select(&block)
end

def eql?(other)
instance_of?(other.class) &&
members.all? { |key| self[key].eql?(other[key]) }
end

def ==(other)
other = coerce(other).first if respond_to?(:coerce, true)
other.kind_of?(self.class) &&
members.all? { |key| self[key] == other[key] }
end

def hash
self.class.hash +
to_a.each_with_index.map {|value, i| (i+1) * value.hash}.sum
end

if keyword_init
def initialize(struct_class_keyword_args = {})
members
@member_values = {}
struct_class_keyword_args.each do |attribute, value|
self[attribute] = value
end
end
else
def initialize(*attribute_values)
members
@member_values = {}
attribute_values.each_with_index do |value, i|
attribute = @members[i]
self[attribute] = value
end
end
end
end
end

ARG_VALIDATION = lambda do |class_name_or_attribute, *attributes|
class_name_or_attribute.nil? || attributes.any?(&:nil?)
end

CLASS_NAME_EXTRACTION = lambda do |class_name_or_attribute|
if class_name_or_attribute.is_a?(String)
raise NameError, "identifier name needs to be constant" unless class_name_or_attribute.match(/^[A-Z]/)
class_name_or_attribute
end
end

def new(class_name_or_attribute, *attributes, keyword_init: false)
raise 'Arguments cannot be nil' if ARG_VALIDATION[class_name_or_attribute, *attributes]
class_name = CLASS_NAME_EXTRACTION[class_name_or_attribute]
attributes.unshift(class_name_or_attribute) if class_name.nil?
attributes = attributes.map(&:to_sym)
struct_class = Class.new(&CLASS_DEFINITION_FOR_ATTRIBUTES[attributes, keyword_init])
class_name.nil? ? struct_class : const_set(class_name, struct_class)
end

end
end
Loading

0 comments on commit babdc6c

Please sign in to comment.