leafy-ruby 0.0.1
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/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +38 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/leafy.gemspec +46 -0
- data/lib/leafy/converter/bool_converter.rb +24 -0
- data/lib/leafy/converter/date_converter.rb +31 -0
- data/lib/leafy/converter/datetime_converter.rb +28 -0
- data/lib/leafy/converter/double_converter.rb +17 -0
- data/lib/leafy/converter/dummy_converter.rb +15 -0
- data/lib/leafy/converter/integer_converter.rb +15 -0
- data/lib/leafy/converter/string_converter.rb +15 -0
- data/lib/leafy/field.rb +27 -0
- data/lib/leafy/field_value.rb +25 -0
- data/lib/leafy/field_value_collection.rb +59 -0
- data/lib/leafy/mixin/active_record/fields.rb +38 -0
- data/lib/leafy/mixin/active_record/schema.rb +37 -0
- data/lib/leafy/mixin/active_record/shared.rb +24 -0
- data/lib/leafy/mixin/data_accessor.rb +41 -0
- data/lib/leafy/mixin/fields.rb +48 -0
- data/lib/leafy/mixin/poro/fields.rb +36 -0
- data/lib/leafy/mixin/poro/schema.rb +31 -0
- data/lib/leafy/mixin/schema.rb +47 -0
- data/lib/leafy/schema.rb +47 -0
- data/lib/leafy/utils.rb +17 -0
- data/lib/leafy/version.rb +16 -0
- data/lib/leafy.rb +37 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0cf163d4cd002f7035394d9997d425b7f0834fc2cc91a42a516f3de9de5c6bfe
|
4
|
+
data.tar.gz: 234f95015a72ad3aacf0db48bfe6c4bc5f7c48db0f1e474c6a88516b9e0a3911
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c4c5342b0a90c406bd6cb4e04969f3e9ec59cb10e03d91e2ea1f91d2db8f6cc125809ee23bade0b098480db82a22cba079d344ff4a599b9db9085cd0ee55b8a4
|
7
|
+
data.tar.gz: 8d6cb3f31e9bab0543e51a3b5344ef6ddd26c1052ad07254b6ef89365fd5ab9a55d92de8472688034da3a51191601ac0924eda53b9bdca497d9890c1e48b482d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
sudo: false
|
2
|
+
|
3
|
+
language: ruby
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.2
|
7
|
+
- 2.3
|
8
|
+
- 2.4
|
9
|
+
- 2.5
|
10
|
+
- ruby-head
|
11
|
+
- jruby-9.2.0.0
|
12
|
+
- jruby-head
|
13
|
+
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
- rvm: jruby-head
|
18
|
+
|
19
|
+
before_install: gem install bundler
|
20
|
+
|
21
|
+
before_script:
|
22
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
23
|
+
- chmod +x ./cc-test-reporter
|
24
|
+
- ./cc-test-reporter before-build
|
25
|
+
|
26
|
+
script: bundle exec rspec
|
27
|
+
|
28
|
+
after_script:
|
29
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
30
|
+
|
31
|
+
cache: bundler
|
32
|
+
|
33
|
+
env:
|
34
|
+
global:
|
35
|
+
CC_TEST_REPORTER_ID=5c94ea74238649cee4b51abbe647de62a3d63971eaeca9aa58c82b62d9a0e6a8
|
36
|
+
COVERAGE=1
|
37
|
+
|
38
|
+
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Evgeny
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# Leafy [](https://travis-ci.org/estepnv/leafy) [](https://codeclimate.com/github/estepnv/leafy/maintainability) [](https://codeclimate.com/github/estepnv/leafy/test_coverage)
|
2
|
+
|
3
|
+
A toolkit for dynamic custom attributes for Ruby applications.
|
4
|
+
|
5
|
+
* Simple modular design - load only things you need
|
6
|
+
* Stored as JSON with your models - allows you to avoid expensive JOIN queries (supports postgresql json/jsonb data types)
|
7
|
+
* Type inference - Infers type from custom field data
|
8
|
+
* Add your own custom field types
|
9
|
+
|
10
|
+
Supported data types:
|
11
|
+
- `string` - strings
|
12
|
+
- `integer` - integer numbers
|
13
|
+
- `double` - floating point numbers
|
14
|
+
- `datetime` - `Time` instances
|
15
|
+
- `date` - `Date` instances
|
16
|
+
- `bool` - `TrueClass` and `FalseClass` instances
|
17
|
+
|
18
|
+
## Quick start
|
19
|
+
|
20
|
+
Add Leafy to Gemfile
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'leafy-ruby'
|
24
|
+
```
|
25
|
+
|
26
|
+
**Plain Ruby app**
|
27
|
+
|
28
|
+
Include "Plain old ruby object" mixin into your class definition to start using leafy
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class SchemaHost < ActiveRecord::Base
|
32
|
+
include Leafy::Mixin::Schema[:poro]
|
33
|
+
|
34
|
+
attr_accessor :leafy_data
|
35
|
+
end
|
36
|
+
|
37
|
+
class FieldsHost < ActiveRecord::Base
|
38
|
+
include Leafy::Mixin::Fields[:poro]
|
39
|
+
|
40
|
+
attr_accessor :leafy_data
|
41
|
+
attr_accessor :leafy_fields
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Schema mixin introduces next methods:
|
46
|
+
|
47
|
+
- `#leafy_fields (Schema)` returns Schema instance allowing you to iterate through custom attribute definitions.
|
48
|
+
- `#leafy_fields=` schema setter method
|
49
|
+
- `#leafy_fields_attributes=` nested attributes setter method
|
50
|
+
|
51
|
+
Fields mixin:
|
52
|
+
|
53
|
+
- `#leafy_values (Hash)` returns a hash representation of your fields data
|
54
|
+
- `#leafy_values=` allows you to assign custom attributes data
|
55
|
+
- `#leafy_fields_values (Leafy::FieldValueCollection)` returns a collection of `Field::Value` instances which provide more control over values data
|
56
|
+
|
57
|
+
**Please note**:
|
58
|
+
Leafy is stateless and changing Schema instance won't reflect on your active record model instance.
|
59
|
+
For changes to take place you have to explicitly assign schema or attributes data to the model.
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
host = SchemaHost.new
|
65
|
+
host.leafy_fields_attributes = [
|
66
|
+
{ name: "Field 1", type: :integer, id: "id_1", metadata: { default: 1, placeholder: "enter an integer", required: true } },
|
67
|
+
{ name: "Field 2", type: :string, id: "id_2", metadata: { default: "", placeholder: "enter value" } },
|
68
|
+
{ name: "Field 3", type: :datetime, id: "id_3", metadata: { order: 10000 } }
|
69
|
+
]
|
70
|
+
|
71
|
+
# or build schema yourself
|
72
|
+
|
73
|
+
field_1 = Leafy::Field.new(name: "Field 1", type: :integer, id: "id_1", default: 1, placeholder: "enter an integer", required: true)
|
74
|
+
field_2 = Leafy::Field.new(name: "Field 2", type: :string, id: "id_2", default: "", placeholder: "enter value")
|
75
|
+
field_3 = Leafy::Field.new(name: "Field 3", type: :datetime, id: "id_3", order: 10000)
|
76
|
+
|
77
|
+
schema = Leafy::Schema.new
|
78
|
+
schema << field_1
|
79
|
+
schema << field_2
|
80
|
+
schema << field_3
|
81
|
+
|
82
|
+
host.leafy_fields = schema
|
83
|
+
|
84
|
+
# after that reference schema for fields target instance
|
85
|
+
|
86
|
+
target = FieldsHost.new
|
87
|
+
target.leafy_fields = host.leafy_fields
|
88
|
+
target.leafy_values
|
89
|
+
|
90
|
+
# => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
|
91
|
+
|
92
|
+
target.leafy_values = { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00"), "junk": "some junk data" }
|
93
|
+
target.leafy_values
|
94
|
+
|
95
|
+
# => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
|
96
|
+
```
|
97
|
+
|
98
|
+
**ActiveRecord**
|
99
|
+
|
100
|
+
Add migration
|
101
|
+
```ruby
|
102
|
+
add_column :schema_hosts, :leafy_data, :text, null: false, default: "{}"
|
103
|
+
add_column :fields_hosts, :leafy_data, :text, null: false, default: "{}"
|
104
|
+
# for postgresql
|
105
|
+
# add_column :leafy_data, :jsonb, null: false, default: {}
|
106
|
+
```
|
107
|
+
|
108
|
+
Update your models
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class SchemaHost < ActiveRecord::Base
|
112
|
+
include Leafy::Mixin::Schema[:active_record]
|
113
|
+
end
|
114
|
+
|
115
|
+
class FieldsHost < ActiveRecord::Base
|
116
|
+
include Leafy::Mixin::Fields[:active_record]
|
117
|
+
|
118
|
+
belongs_to :schema_host, required: true
|
119
|
+
delegate :leafy_fields, to: :schema_host
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
host = SchemaHost.create(
|
125
|
+
leafy_fields_attributes: [
|
126
|
+
{ name: "Field 1", type: :integer, id: "id_1", metadata: { default: 1, placeholder: "enter an integer", required: true } },
|
127
|
+
{ name: "Field 2", type: :string, id: "id_2", metadata: { default: "", placeholder: "enter value" } },
|
128
|
+
{ name: "Field 3", type: :datetime, id: "id_3", metadata: { order: 10000 } }
|
129
|
+
]
|
130
|
+
)
|
131
|
+
|
132
|
+
target = FieldsHost.create(schema_host: host)
|
133
|
+
target.leafy_values
|
134
|
+
|
135
|
+
# => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
|
136
|
+
|
137
|
+
target.leafy_values = { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00"), "junk": "some junk data" }
|
138
|
+
target.save!
|
139
|
+
target.reload
|
140
|
+
|
141
|
+
target.leafy_values
|
142
|
+
|
143
|
+
# => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
|
144
|
+
```
|
145
|
+
|
146
|
+
## Adding your own types
|
147
|
+
|
148
|
+
Leafy allows adding your own data types
|
149
|
+
To allow leafy process your own data type you need to describe how to store it. For that purpose leafy utilizes converter classes associated for each type.
|
150
|
+
|
151
|
+
Converter instance has to implement `#dump` and `#load` methods
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class MyComplexTypeConverter
|
155
|
+
def self.load(json_string)
|
156
|
+
# parsing logic
|
157
|
+
return MyComplexType.new(parsed_data)
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.dump(my_complex_type_instance)
|
161
|
+
# serializing logic
|
162
|
+
return json
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
Leafy.register_converter(:complex_type, MyComplexTypeConverter)
|
167
|
+
```
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
## Contributing
|
172
|
+
|
173
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/estepnv/leafy.
|
174
|
+
|
175
|
+
## License
|
176
|
+
|
177
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "leafy"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/leafy.gemspec
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "leafy/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "leafy-ruby"
|
8
|
+
spec.version = Leafy.version
|
9
|
+
spec.authors = ["Evgeny Stepanov"]
|
10
|
+
spec.email = ["estepnv@icloud.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Toolkit for custom attributes in Ruby apps}
|
13
|
+
spec.description = <<-DESC
|
14
|
+
Leafy is toolkit that allows you to integrate dynamic custom attributes functionality into your ruby application.
|
15
|
+
|
16
|
+
It provides high level API you can use for any suitable use-case.
|
17
|
+
It ships with several basic data types and allows you to add your own data type converters.
|
18
|
+
DESC
|
19
|
+
|
20
|
+
spec.homepage = "https://gibhub.com/estepnv/leafy"
|
21
|
+
spec.license = "MIT"
|
22
|
+
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "simplecov"
|
34
|
+
|
35
|
+
if RUBY_VERSION >= "2.2.2"
|
36
|
+
spec.add_development_dependency "activerecord", "~> 5.0"
|
37
|
+
else
|
38
|
+
spec.add_development_dependency "activerecord", "~> 4.2"
|
39
|
+
end
|
40
|
+
|
41
|
+
if RUBY_ENGINE == "jruby"
|
42
|
+
spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "51"
|
43
|
+
else
|
44
|
+
spec.add_development_dependency "sqlite3"
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
module Converter
|
5
|
+
class BoolConverter
|
6
|
+
def dump(value)
|
7
|
+
target = value
|
8
|
+
target = load(target) unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
9
|
+
|
10
|
+
target ? "1" : "0"
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(value)
|
14
|
+
return if value.nil?
|
15
|
+
|
16
|
+
target = value.respond_to?(:downcase) ? (value.downcase rescue nil) : value
|
17
|
+
return true if ["1", "true", "t", 1, "yes", "y", true].include?(target)
|
18
|
+
return false if ["0", "false", "f", 0, "no", "n", false].include?(target)
|
19
|
+
|
20
|
+
raise(ArgumentError, "can't parse value to bool: #{value}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Converter
|
7
|
+
class DateConverter
|
8
|
+
|
9
|
+
def dump(value)
|
10
|
+
return if value.nil?
|
11
|
+
|
12
|
+
target = value.dup
|
13
|
+
target = load(target) if target.is_a?(String)
|
14
|
+
target = target.dup.to_date if target.is_a?(Time)
|
15
|
+
|
16
|
+
unless target.is_a?(Date)
|
17
|
+
raise(ArgumentError, "is not a Date object")
|
18
|
+
end
|
19
|
+
|
20
|
+
target.iso8601
|
21
|
+
end
|
22
|
+
|
23
|
+
def load(value)
|
24
|
+
return if value.nil?
|
25
|
+
return value if value.is_a?(Date)
|
26
|
+
Date.parse(value)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Converter
|
7
|
+
class DatetimeConverter
|
8
|
+
|
9
|
+
def dump(value)
|
10
|
+
return if value.nil?
|
11
|
+
|
12
|
+
target = value.dup
|
13
|
+
target = load(target) if target.is_a?(String)
|
14
|
+
|
15
|
+
raise(ArgumentError, "is not a Time object") unless target.is_a?(Time)
|
16
|
+
|
17
|
+
target.utc.iso8601
|
18
|
+
end
|
19
|
+
|
20
|
+
def load(value)
|
21
|
+
return if value.nil?
|
22
|
+
return value if value.is_a?(Time)
|
23
|
+
Time.parse(value).utc
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/leafy/field.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Leafy
|
5
|
+
class Field
|
6
|
+
attr_accessor :id, :name, :type, :metadata
|
7
|
+
|
8
|
+
def initialize(attributes = {})
|
9
|
+
raise ArgumentError, "attributes is not a Hash" unless attributes.is_a?(Hash)
|
10
|
+
attributes = Leafy::Utils.symbolize_keys(attributes)
|
11
|
+
|
12
|
+
self.name = attributes[:name]
|
13
|
+
self.type = attributes[:type]
|
14
|
+
self.id = attributes.fetch(:id) { [name.downcase.strip.tr(" ", "-"), SecureRandom.uuid].join("-") }
|
15
|
+
self.metadata = attributes.fetch(:metadata, {})
|
16
|
+
end
|
17
|
+
|
18
|
+
def serializable_hash
|
19
|
+
{
|
20
|
+
name: name,
|
21
|
+
type: type,
|
22
|
+
id: id,
|
23
|
+
metadata: metadata
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
class FieldValue
|
5
|
+
attr_accessor :id, :name, :type, :raw, :converter
|
6
|
+
|
7
|
+
def initialize(attributes)
|
8
|
+
attributes = attributes.dup.to_a.map { |pair| [pair[0].to_sym, pair[1]]}.to_h
|
9
|
+
|
10
|
+
self.id = attributes.fetch(:id)
|
11
|
+
self.name = attributes.fetch(:name)
|
12
|
+
self.type = attributes.fetch(:type)
|
13
|
+
self.converter = attributes.fetch(:converter) { Leafy.converters.fetch(type.to_sym) { raise(RuntimeError, "unregistered converter #{type}") } }
|
14
|
+
self.raw = attributes.fetch(:raw)
|
15
|
+
end
|
16
|
+
|
17
|
+
def value
|
18
|
+
converter.load(raw)
|
19
|
+
end
|
20
|
+
|
21
|
+
def value=(val)
|
22
|
+
self.raw = converter.dump(converter.load(val))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
class FieldValueCollection
|
5
|
+
include ::Enumerable
|
6
|
+
|
7
|
+
def each
|
8
|
+
if block_given?
|
9
|
+
@leafy_field_values.each { |i| yield i }
|
10
|
+
else
|
11
|
+
@leafy_field_values.each
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](index)
|
16
|
+
to_a[index]
|
17
|
+
end
|
18
|
+
|
19
|
+
def size
|
20
|
+
count
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(leafy_fields, values = {})
|
24
|
+
@leafy_fields = leafy_fields
|
25
|
+
@leafy_field_values = leafy_fields.map do |custom_field|
|
26
|
+
Leafy::FieldValue.new(
|
27
|
+
id: custom_field.id,
|
28
|
+
name: custom_field.name,
|
29
|
+
raw: values[custom_field.id],
|
30
|
+
type: custom_field.type
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def values
|
36
|
+
inject({}) do |acc, field_value|
|
37
|
+
acc[field_value.id] = field_value.value
|
38
|
+
acc
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def values=(attributes = {})
|
43
|
+
attributes = attributes.dup.to_a.map { |pair| [pair[0].to_s, pair[1]]}.to_h
|
44
|
+
|
45
|
+
@leafy_field_values.each do |field_value|
|
46
|
+
field_value.value = attributes[field_value.id]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.dump(field_values_collection)
|
51
|
+
JSON.dump(field_values_collection.map { |field_value| [field_value.id, field_value.raw] }.to_h)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.load(leafy_fields, data)
|
55
|
+
Leafy::FieldValueCollection.new(leafy_fields, JSON.load(data))
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Mixin
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
module Fields
|
10
|
+
module ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
def leafy_fields
|
16
|
+
raise(RuntimeError, "Leafy: leafy_fields method is not defined")
|
17
|
+
end
|
18
|
+
|
19
|
+
def leafy_values
|
20
|
+
leafy_field_values.values
|
21
|
+
end
|
22
|
+
|
23
|
+
def leafy_values=(attributes = {})
|
24
|
+
field_value_list = leafy_field_values
|
25
|
+
field_value_list.values = attributes
|
26
|
+
|
27
|
+
self.leafy_data = ::Leafy::FieldValueCollection.dump(field_value_list)
|
28
|
+
end
|
29
|
+
|
30
|
+
def leafy_field_values
|
31
|
+
::Leafy::FieldValueCollection.load(leafy_fields, leafy_data || "{}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Mixin
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
module Schema
|
10
|
+
module InstanceMethods
|
11
|
+
def leafy_fields
|
12
|
+
data = _leafy_data
|
13
|
+
|
14
|
+
activerecord_json_column? ?
|
15
|
+
::Leafy::Schema.new(data) :
|
16
|
+
::Leafy::Schema.load(data.nil? ? "[]" : data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def leafy_fields=(leafy_schema)
|
20
|
+
self._leafy_data = activerecord_json_column? ?
|
21
|
+
leafy_schema.serializable_hash :
|
22
|
+
::Leafy::Schema.dump(leafy_schema)
|
23
|
+
end
|
24
|
+
|
25
|
+
def leafy_fields_attributes=(attributes_list)
|
26
|
+
self.leafy_fields = ::Leafy::Schema.new(attributes_list)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Mixin
|
7
|
+
module ActiveRecord
|
8
|
+
module Shared
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
private
|
12
|
+
|
13
|
+
def activerecord_json_column?
|
14
|
+
return false unless self.is_a?(::ActiveRecord::Base)
|
15
|
+
return false unless column = self.class.columns_hash[self.class.leafy_data_attribute.to_s]
|
16
|
+
|
17
|
+
[:json, :jsonb].include?(column.type)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Leafy
|
2
|
+
module Mixin
|
3
|
+
module DataAccessor
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def leafy_data_attribute=(value)
|
7
|
+
@leafy_data_attribute = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def leafy_data_attribute
|
11
|
+
@leafy_data_attribute ||= :leafy_data
|
12
|
+
end
|
13
|
+
|
14
|
+
def leafy_data_getter; leafy_data_attribute; end
|
15
|
+
|
16
|
+
def leafy_data_setter; "#{leafy_data_attribute}="; end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
private
|
21
|
+
|
22
|
+
def _leafy_data=(value)
|
23
|
+
unless respond_to?(self.class.leafy_data_setter)
|
24
|
+
raise(RuntimeError, "must respond to ##{self.class.leafy_data_setter}") unless respond_to?(:leafy_data=)
|
25
|
+
end
|
26
|
+
|
27
|
+
public_send(self.class.leafy_data_setter, value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def _leafy_data
|
31
|
+
unless respond_to?(self.class.leafy_data_getter)
|
32
|
+
raise(RuntimeError, "must respond to ##{self.class.leafy_data_getter}")
|
33
|
+
end
|
34
|
+
|
35
|
+
public_send(self.class.leafy_data_getter)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./data_accessor"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Mixin
|
7
|
+
module Fields
|
8
|
+
|
9
|
+
def self.[](orm = :poro)
|
10
|
+
Module.new do
|
11
|
+
@orm = orm
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
|
15
|
+
base.extend DataAccessor::ClassMethods
|
16
|
+
base.include DataAccessor::InstanceMethods
|
17
|
+
|
18
|
+
case @orm
|
19
|
+
|
20
|
+
when :poro
|
21
|
+
|
22
|
+
require_relative "./poro/fields"
|
23
|
+
|
24
|
+
base.extend Poro::Fields::ClassMethods
|
25
|
+
base.include Poro::Fields::InstanceMethods
|
26
|
+
|
27
|
+
when :active_record
|
28
|
+
|
29
|
+
require_relative "./active_record/shared"
|
30
|
+
require_relative "./active_record/fields"
|
31
|
+
|
32
|
+
base.extend ActiveRecord::Fields::ClassMethods
|
33
|
+
base.include ActiveRecord::Shared::InstanceMethods
|
34
|
+
base.include ActiveRecord::Fields::InstanceMethods
|
35
|
+
|
36
|
+
else
|
37
|
+
|
38
|
+
raise(RuntimeError, "Leafy: unsupported schema storage: #{orm}")
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
module Mixin
|
5
|
+
module Poro
|
6
|
+
|
7
|
+
module Fields
|
8
|
+
module ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
|
13
|
+
def leafy_fields
|
14
|
+
raise(RuntimeError, "Leafy: leafy_fields method is not defined")
|
15
|
+
end
|
16
|
+
|
17
|
+
def leafy_values
|
18
|
+
leafy_field_values.values
|
19
|
+
end
|
20
|
+
|
21
|
+
def leafy_values=(attributes = {})
|
22
|
+
field_value_list = leafy_field_values
|
23
|
+
field_value_list.values = attributes
|
24
|
+
|
25
|
+
self._leafy_data = ::Leafy::FieldValueCollection.dump(field_value_list)
|
26
|
+
end
|
27
|
+
|
28
|
+
def leafy_field_values
|
29
|
+
::Leafy::FieldValueCollection.load(leafy_fields, _leafy_data || "{}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
module Mixin
|
5
|
+
module Poro
|
6
|
+
|
7
|
+
module Schema
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def leafy_fields
|
11
|
+
data = _leafy_data
|
12
|
+
Leafy::Schema.load(data.nil? ? "[]" : data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def leafy_fields=(leafy_schema)
|
16
|
+
self._leafy_data = ::Leafy::Schema.dump(leafy_schema)
|
17
|
+
end
|
18
|
+
|
19
|
+
def leafy_fields_attributes=(attributes_list)
|
20
|
+
self.leafy_fields = ::Leafy::Schema.new(attributes_list)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./data_accessor"
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Mixin
|
7
|
+
module Schema
|
8
|
+
|
9
|
+
def self.[](orm)
|
10
|
+
Module.new do
|
11
|
+
@orm = orm
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
|
15
|
+
base.extend DataAccessor::ClassMethods
|
16
|
+
base.include DataAccessor::InstanceMethods
|
17
|
+
|
18
|
+
case @orm
|
19
|
+
when :poro
|
20
|
+
|
21
|
+
require_relative "./poro/schema"
|
22
|
+
|
23
|
+
base.extend Poro::Schema::ClassMethods
|
24
|
+
base.include Poro::Schema::InstanceMethods
|
25
|
+
|
26
|
+
when :active_record
|
27
|
+
|
28
|
+
require_relative "./active_record/shared"
|
29
|
+
require_relative "./active_record/schema"
|
30
|
+
|
31
|
+
base.extend ActiveRecord::Schema::ClassMethods
|
32
|
+
base.include ActiveRecord::Shared::InstanceMethods
|
33
|
+
base.include ActiveRecord::Schema::InstanceMethods
|
34
|
+
|
35
|
+
else
|
36
|
+
|
37
|
+
raise(RuntimeError, "Leafy: unsupported schema storage: #{orm}")
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/leafy/schema.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Leafy
|
5
|
+
class Schema
|
6
|
+
include ::Enumerable
|
7
|
+
|
8
|
+
def each
|
9
|
+
if block_given?
|
10
|
+
@fields.each { |i| yield i }
|
11
|
+
else
|
12
|
+
@fields.each
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(fields = [])
|
17
|
+
@fields = fields.map { |field| Leafy::Field.new(field) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def ids
|
21
|
+
@fields.map(&:id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](identifier)
|
25
|
+
@fields.find { |f| f.id == identifier }
|
26
|
+
end
|
27
|
+
|
28
|
+
def push(field)
|
29
|
+
raise(ArgumentError, "is not a field") unless field.is_a?(Leafy::Field)
|
30
|
+
@fields << field
|
31
|
+
end
|
32
|
+
|
33
|
+
def serializable_hash
|
34
|
+
@fields.map(&:serializable_hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.dump(schema)
|
38
|
+
JSON.dump(schema.serializable_hash)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.load(data)
|
42
|
+
Schema.new(JSON.parse(data))
|
43
|
+
end
|
44
|
+
|
45
|
+
alias :<< :push
|
46
|
+
end
|
47
|
+
end
|
data/lib/leafy/utils.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Leafy
|
2
|
+
module Utils
|
3
|
+
def self.symbolize_keys(hash)
|
4
|
+
hash.dup.to_a.map do |pair|
|
5
|
+
key, value = pair
|
6
|
+
[key.to_sym, value.is_a?(Hash) ? Leafy::Utils.symbolize_keys(value) : value]
|
7
|
+
end.to_h
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.stringify_keys(hash)
|
11
|
+
hash.dup.to_a.map do |pair|
|
12
|
+
key, value = pair
|
13
|
+
[key.to_s, value.is_a?(Hash) ? Leafy::Utils.stringify_keys(value) : value]
|
14
|
+
end.to_h
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/leafy.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "leafy/version"
|
4
|
+
require "leafy/utils"
|
5
|
+
require "leafy/field"
|
6
|
+
require "leafy/schema"
|
7
|
+
require "leafy/field_value"
|
8
|
+
require "leafy/field_value_collection"
|
9
|
+
Dir[File.expand_path("../leafy/converter/**/*.rb", __FILE__)].each { |f| require f }
|
10
|
+
require "leafy/mixin/schema"
|
11
|
+
require "leafy/mixin/fields"
|
12
|
+
|
13
|
+
|
14
|
+
# module definition
|
15
|
+
module Leafy
|
16
|
+
def self.register_converter(name, converter)
|
17
|
+
raise(ArgumentError, "converter is not provided") if converter.nil?
|
18
|
+
|
19
|
+
if !converter.respond_to?(:load) || !converter.respond_to?(:dump)
|
20
|
+
raise(ArgumentError, "converter must respond to #dump and #load")
|
21
|
+
end
|
22
|
+
|
23
|
+
converters[name] = converter
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.converters
|
27
|
+
@converters ||= {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Leafy.register_converter(:dummy, Leafy::Converter::DummyConverter.new)
|
32
|
+
Leafy.register_converter(:string, Leafy::Converter::StringConverter.new)
|
33
|
+
Leafy.register_converter(:integer, Leafy::Converter::IntegerConverter.new)
|
34
|
+
Leafy.register_converter(:double, Leafy::Converter::DoubleConverter.new)
|
35
|
+
Leafy.register_converter(:datetime, Leafy::Converter::DatetimeConverter.new)
|
36
|
+
Leafy.register_converter(:bool, Leafy::Converter::BoolConverter.new)
|
37
|
+
Leafy.register_converter(:date, Leafy::Converter::DateConverter.new)
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: leafy-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Evgeny Stepanov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: |2
|
98
|
+
Leafy is toolkit that allows you to integrate dynamic custom attributes functionality into your ruby application.
|
99
|
+
|
100
|
+
It provides high level API you can use for any suitable use-case.
|
101
|
+
It ships with several basic data types and allows you to add your own data type converters.
|
102
|
+
email:
|
103
|
+
- estepnv@icloud.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- ".gitignore"
|
109
|
+
- ".rspec"
|
110
|
+
- ".travis.yml"
|
111
|
+
- Gemfile
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- bin/console
|
116
|
+
- bin/setup
|
117
|
+
- leafy.gemspec
|
118
|
+
- lib/leafy.rb
|
119
|
+
- lib/leafy/converter/bool_converter.rb
|
120
|
+
- lib/leafy/converter/date_converter.rb
|
121
|
+
- lib/leafy/converter/datetime_converter.rb
|
122
|
+
- lib/leafy/converter/double_converter.rb
|
123
|
+
- lib/leafy/converter/dummy_converter.rb
|
124
|
+
- lib/leafy/converter/integer_converter.rb
|
125
|
+
- lib/leafy/converter/string_converter.rb
|
126
|
+
- lib/leafy/field.rb
|
127
|
+
- lib/leafy/field_value.rb
|
128
|
+
- lib/leafy/field_value_collection.rb
|
129
|
+
- lib/leafy/mixin/active_record/fields.rb
|
130
|
+
- lib/leafy/mixin/active_record/schema.rb
|
131
|
+
- lib/leafy/mixin/active_record/shared.rb
|
132
|
+
- lib/leafy/mixin/data_accessor.rb
|
133
|
+
- lib/leafy/mixin/fields.rb
|
134
|
+
- lib/leafy/mixin/poro/fields.rb
|
135
|
+
- lib/leafy/mixin/poro/schema.rb
|
136
|
+
- lib/leafy/mixin/schema.rb
|
137
|
+
- lib/leafy/schema.rb
|
138
|
+
- lib/leafy/utils.rb
|
139
|
+
- lib/leafy/version.rb
|
140
|
+
homepage: https://gibhub.com/estepnv/leafy
|
141
|
+
licenses:
|
142
|
+
- MIT
|
143
|
+
metadata: {}
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 2.7.7
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Toolkit for custom attributes in Ruby apps
|
164
|
+
test_files: []
|