leafy-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea/*
10
+ /coverage/*
11
+ .rbenv-gemsets
12
+ Gemfile.lock
13
+ # rspec failure tracking
14
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --
3
+ --require spec_helper
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
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in leafy.gemspec
6
+ gemspec
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 [![Build Status](https://travis-ci.org/estepnv/leafy.svg?branch=master)](https://travis-ci.org/estepnv/leafy) [![Maintainability](https://api.codeclimate.com/v1/badges/5108d8a1ac5e2915f30f/maintainability)](https://codeclimate.com/github/estepnv/leafy/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/5108d8a1ac5e2915f30f/test_coverage)](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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class DoubleConverter
6
+ def dump(value)
7
+ return if value.nil?
8
+ value.to_s
9
+ end
10
+
11
+ def load(value)
12
+ return if value.nil?
13
+ value.to_f
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class DummyConverter
6
+ def dump(value)
7
+ value
8
+ end
9
+
10
+ def load(value)
11
+ value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class IntegerConverter
6
+ def dump(value)
7
+ value.nil? ? nil : value.to_s
8
+ end
9
+
10
+ def load(value)
11
+ value.nil? ? nil : value.to_i
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class StringConverter
6
+ def dump(value)
7
+ value
8
+ end
9
+
10
+ def load(value)
11
+ value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ def self.version
5
+ Gem::Version.new VERSION::STRING
6
+ end
7
+
8
+ module VERSION
9
+ MAJOR = 0
10
+ MINOR = 0
11
+ TINY = 1
12
+ PRE = nil
13
+
14
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
+ end
16
+ 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: []