ymodel 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb2619f40ddda987fd2634ac4a76cd6622856ac0c2797dcf05853de00f6e8894
4
- data.tar.gz: 81188efca59a5169ff6a74b25473e35aa87407c2080b9125b1a13739c9b1e964
3
+ metadata.gz: 3e76b055a94aca7267be60ff8727343c1cacdbee176df49d0b757c63678c3703
4
+ data.tar.gz: 279c251a3f1bc448b4ca013ead8bee6eb2a3356f197b415360d099ea16a33762
5
5
  SHA512:
6
- metadata.gz: 866c550c19239e4ca5c64be3d50fce87c18d3e4a41d9d0298b2d271dd09923e9edcaef4278d01330bddde83e0fb6ec9aadaced36a0f66e800f2e54700639aeb4
7
- data.tar.gz: c6056b25f8809ab20a30d15625fb8f811e4e3e8fb95b79b5c1d17cdf5bd1e92ef2a7b0d8f7f066fe2174de54a6f35dd1e960b7f282dfd5a94d83c563c7966f97
6
+ metadata.gz: 9065024111d3d97779cc30566e08d14d118f797ace4c9bfc869255ed5558f7078601fe95bfaaeb1b7a1c3079fefab5b64db48f3c593ddddaee7ecc942a44663f
7
+ data.tar.gz: 45228ad8ac261538e73b064544780cf4b96036a95e75f1860af99d91e61226c6eea0806079e3ac9b79740552ba9993862952beba52740bc58fafcf324dbef7ba
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ymodel (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.5.0)
10
+ rake (13.0.6)
11
+ rspec (3.11.0)
12
+ rspec-core (~> 3.11.0)
13
+ rspec-expectations (~> 3.11.0)
14
+ rspec-mocks (~> 3.11.0)
15
+ rspec-core (3.11.0)
16
+ rspec-support (~> 3.11.0)
17
+ rspec-expectations (3.11.0)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.11.0)
20
+ rspec-mocks (3.11.1)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.11.0)
23
+ rspec-support (3.11.0)
24
+
25
+ PLATFORMS
26
+ arm64-darwin-20
27
+
28
+ DEPENDENCIES
29
+ rake (~> 13.0)
30
+ rspec (~> 3.0)
31
+ ymodel!
32
+
33
+ BUNDLED WITH
34
+ 2.3.14
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Ymodel
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ymodel`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Welcome to ymodel. An active-record-like interface to wrap yaml files.
6
4
 
7
5
  ## Installation
8
6
 
@@ -16,7 +14,61 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
14
 
17
15
  ## Usage
18
16
 
19
- TODO: Write usage instructions here
17
+ Create a model with data from Yaml files:
18
+
19
+ ```ruby
20
+ class GridCode < YModel::Base
21
+ index_on :name
22
+
23
+ default_attribute :successful_outcomes, with: []
24
+ default_attribute :loading_outcomes, with: []
25
+ default_attribute :loading_maximum, with: 0
26
+ default_attribute :required_documents, with: []
27
+ end
28
+ ```
29
+
30
+ ```yaml
31
+ ---
32
+ data:
33
+ - name: FM1
34
+ successful_questionnaire_outcomes:
35
+ - standard
36
+ - accord
37
+ loading_outcomes:
38
+ - majoration
39
+ loading_maximum: 50
40
+ questionnaire_type: QSS
41
+ priority: 1
42
+
43
+ - name: FM2
44
+ successful_questionnaire_outcomes:
45
+ - standard
46
+ - accord
47
+ loading_outcomes:
48
+ - majoration
49
+ loading_maximum: 50
50
+ questionnaire_type: QM
51
+ priority: 1
52
+
53
+ - name: FM3
54
+ questionnaire_type: QM
55
+ priority: 2
56
+ required_documents:
57
+ - BIO 1
58
+ ```
59
+
60
+ ```irb
61
+ 3.0.1 :001 > GridCode.all.first
62
+ =>
63
+ #<GridCode:0x000000011a0b7640
64
+ @loading_maximum=50,
65
+ @loading_outcomes=["majoration"],
66
+ @name="FM1",
67
+ @priority=1,
68
+ @questionnaire_type="QSS",
69
+ @required_documents=[],
70
+ @successful_questionnaire_outcomes=["standard", "accord"]>
71
+ ```
20
72
 
21
73
  ## Development
22
74
 
@@ -26,7 +78,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
26
78
 
27
79
  ## Contributing
28
80
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ymodel.
81
+ Suggestions, ideas or contribution can be discussed with steven@remarkgroup.com.
30
82
 
31
83
  ## License
32
84
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'relatable'
4
+ require_relative 'loadable'
5
+ require_relative 'triggerable'
6
+ require_relative 'queryable'
7
+
8
+ module Ymodel
9
+ # This is used to wrap a YAML file in a similar manner to ActiveRecord
10
+ # wrapping a database.
11
+ class Base
12
+ extend Ymodel::Relatable
13
+ extend Ymodel::Loadable
14
+ extend Ymodel::Triggerable
15
+ extend Ymodel::Queryable
16
+
17
+ def initialize(record = {})
18
+ record.each do |k, v|
19
+ instance_variable_set "@#{k}", v if attribute?(k)
20
+ end
21
+ end
22
+
23
+ def ==(other)
24
+ other.respond_to?(:attributes) && attributes == other.attributes
25
+ end
26
+
27
+ def attributes
28
+ # definitive schema
29
+ schema.attributes
30
+ .each_with_object({}) { |attr, memo| memo[attr] = send(attr) }
31
+ end
32
+
33
+ def [](property)
34
+ instance_variable_get("@#{property}")
35
+ end
36
+
37
+ def attribute?(key)
38
+ # definitive schema
39
+ schema.include?(key) || key == :id
40
+ end
41
+
42
+ def index
43
+ self[index_key]
44
+ end
45
+
46
+ private
47
+
48
+ def schema
49
+ self.class.schema
50
+ end
51
+
52
+ def index_key
53
+ self.class.instance_variable_get('@index') || :id
54
+ end
55
+
56
+ def index_set?
57
+ # The name of the key of the index is stored as an instance variable in
58
+ # the class.
59
+ index = self.class.instance_variable_get('@index') || :id
60
+ self[index]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/Documentation
4
+ module Ymodel
5
+ module Errors; end
6
+
7
+ class YmodelError < StandardError
8
+ end
9
+
10
+ class SourceFileNotFound < YmodelError
11
+ attr_reader :model
12
+
13
+ def initialize(message = nil, model = nil)
14
+ @model = model
15
+
16
+ super(message)
17
+ end
18
+ end
19
+
20
+ class MissingConstant < YmodelError
21
+ end
22
+
23
+ class UnacceptableOptionsError < YmodelError
24
+ end
25
+
26
+ class UnpermittedParamsError < YmodelError
27
+ end
28
+
29
+ class RecordNotFound < YmodelError
30
+ end
31
+
32
+ class DuplicateIndexError < YmodelError
33
+ end
34
+ end
35
+ # rubocop:enable Style/Documentation
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ymodel
4
+ # This module contains YModel logic for managing relations.
5
+ module Helper
6
+ def self.model_class(model)
7
+ as_const = model.to_s.singularize.camelcase
8
+ Kernel.const_get(as_const)
9
+ rescue StandardError
10
+ message = "relation `#{model}` couldn't be made because constant "\
11
+ "`#{as_const}` doesn't exist."
12
+ raise Ymodel::MissingConstant, message
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema'
4
+
5
+ module Ymodel
6
+ # Module resposible for loading the schema and data from the yaml files.
7
+ module Loadable
8
+ # This method can be called from within a concrete implementation to
9
+ # overwrite the default ymodel filename associated with that model.
10
+ def source_file(filename)
11
+ @source = filename
12
+
13
+ # Similar to Trigger#inherited. A hook for loading the schema.
14
+ define_readers self
15
+ end
16
+
17
+ def pears_subject(subject)
18
+ @pears = true
19
+ if block_given?
20
+ @source = Pears.subject(subject) { |provider| yield(provider) }
21
+ else
22
+ @source = subject
23
+ end
24
+ # Similar to Trigger#inherited. A hook for loading the schema.
25
+ define_readers self
26
+ end
27
+
28
+ # With is used as the default value of the attribute
29
+ def default_attribute(attr_name, with: nil)
30
+ @schema << attr_name
31
+ define_reader attr_name, default: with
32
+ end
33
+
34
+ def schema
35
+ @schema = Schema.new(records, index)
36
+ if @manual_index
37
+ @schema ||= Schema.new(records)
38
+ else
39
+ @schema ||= Schema.new(records, :id)
40
+ end
41
+ rescue Errno::ENOENT => e
42
+ Schema.new({})
43
+ end
44
+
45
+ def load_records!
46
+ if load_indices?
47
+ all = records.each_with_index
48
+ .map { |record, i| new(record.merge({id: i})) }
49
+ else
50
+ all = records.map { |record| new(record) }
51
+ end
52
+ unless all.map(&:index) == all.map(&:index).uniq
53
+ raise DuplicateIndexError,
54
+ "#{name}: Some records share the same index"
55
+ end
56
+
57
+ all
58
+ end
59
+
60
+ def index_on(key)
61
+ @manual_index = true
62
+ @index = key
63
+ end
64
+
65
+ def index
66
+ @index || :id
67
+ end
68
+
69
+ def source_files
70
+ if compiled?
71
+ Dir.glob(File.join(source, '*.yml'))
72
+ else
73
+ [@source]
74
+ end
75
+ end
76
+
77
+ def define_reader(attribute, default: nil)
78
+ define_method(attribute.to_sym) do
79
+ value = instance_variable_get("@#{attribute}")
80
+ if value.nil? && !default.nil?
81
+ value = instance_variable_set(
82
+ "@#{attribute}",
83
+ default.respond_to?(:call) ? default.call : default
84
+ )
85
+ end
86
+
87
+ value
88
+ end
89
+ end
90
+
91
+ protected
92
+
93
+ def records
94
+ if @pears
95
+ @source[:data]
96
+ else
97
+ @records ||= source_files.flat_map { |name| YAML.load_file(name) }
98
+ .map(&:symbolize_keys)
99
+ end
100
+ end
101
+
102
+ def source
103
+ @source ||= _source
104
+ end
105
+
106
+ private
107
+
108
+ def load_indices?
109
+ index == :id && !@manual_index
110
+ end
111
+
112
+ def compiled?
113
+ File.directory?(File.join(Rails.root, source))
114
+ end
115
+
116
+ def _source
117
+ File.join(Rails.root,
118
+ 'config',
119
+ 'ymodel',
120
+ name.gsub('::', '/').pluralize.underscore + '.yml')
121
+ end
122
+
123
+ def load_index=(value)
124
+ @load_index = value
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ymodel
4
+ # This mixin holds the query methods.
5
+ module Queryable
6
+ def find(index)
7
+ all.find { |record| record.index == index }
8
+ end
9
+
10
+ def find!(index)
11
+ find(index) || raise_record_not_found_exception!(index)
12
+ end
13
+
14
+ def find_by(attributes)
15
+ sanitized = sanitize_attributes(attributes)
16
+ all.each do |record|
17
+ return record if sanitized.all? { |k, v| record.send(k) == v }
18
+ end
19
+
20
+ nil
21
+ end
22
+
23
+ def find_by!(attributes)
24
+ find_by(attributes) || raise_record_not_found_exception!(attributes)
25
+ end
26
+
27
+ def find_by_key(key)
28
+ all.find do |record|
29
+ record.key == key.to_s
30
+ end
31
+ end
32
+
33
+ def find_by_key!(key)
34
+ find_by_key(key) || raise_record_not_found_exception!(key)
35
+ end
36
+
37
+ def all
38
+ if @pears
39
+ load_records!
40
+ else
41
+ @all ||= load_records!
42
+ end
43
+ rescue Errno::ENOENT
44
+ raise SourceFileNotFound
45
+ end
46
+
47
+ def where(attributes)
48
+ sanitized = sanitize_attributes(attributes)
49
+
50
+ if sanitized.length != attributes.length
51
+ unpermitted = (attributes.keys.map(&:to_sym) - sanitized.keys)
52
+ message = "These attributes are not allowed: #{unpermitted}"
53
+
54
+ raise UnpermittedParamsError, message
55
+ end
56
+
57
+ all.select do |record|
58
+ sanitized.all? { |key, value| record.send(key) == value }
59
+ end
60
+ end
61
+
62
+ # Beware of using this method. If all attributes get removed during
63
+ # sanitation it returns the equivalent of #all.
64
+ def where!(attributes)
65
+ sanitized = sanitize_attributes(attributes)
66
+ all.select do |record|
67
+ sanitized.all? { |key, value| record.send(key) == value }
68
+ end
69
+ end
70
+
71
+ def sanitize_attributes(attributes)
72
+ # definitive schema
73
+ attributes.symbolize_keys!
74
+ .select { |attr| schema.include?(attr) }
75
+ end
76
+
77
+ def raise_record_not_found_exception!(attributes = nil)
78
+ if attributes.is_a?(Hash)
79
+ message = "Couldn't find #{name} with "
80
+ attributes.each { |k, v| message += "#{k}: #{v}" }
81
+ else
82
+ message = "Couldn't find #{name} #{attributes}"
83
+ end
84
+ raise RecordNotFound, message
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require_relative 'helper'
5
+
6
+ module Ymodel
7
+ # This module contains YModel logic for managing relations.
8
+ module Relatable
9
+ # This method is used to define the key on which relations are build.
10
+ # We default to 'id'.
11
+
12
+ def belongs_to(model, options = {})
13
+ define_method(model) do
14
+ related_class =
15
+ Helper.model_class(options[:class_name] || model)
16
+ key =
17
+ if related_class < Base
18
+ :"#{model.to_s.singularize}_#{related_class.index}"
19
+ else
20
+ :"#{related_class.to_s.singularize}_id"
21
+ end
22
+ related_class.find(self[key])
23
+ end
24
+ end
25
+
26
+ # rubocop:disable Naming/PredicateName
27
+ # rubocop:disable Naming/UncommunicativeMethodParamName
28
+ def has_many(model, class_name: nil, as: nil, foreign_key: nil)
29
+ raise_options_error if as && foreign_key
30
+
31
+ foreign_key ||= default_foreign_key
32
+ define_method(model) do
33
+ relation_class = Helper.model_class(class_name || model)
34
+ if as
35
+ relation_class.where("#{as}_id" => id,
36
+ "#{as}_type" => self.class.name)
37
+ else
38
+ relation_class.where(foreign_key.to_sym => index)
39
+ end
40
+ end
41
+ end
42
+
43
+ def has_one(model, class_name: nil, as: nil, foreign_key: nil)
44
+ raise_options_error if as && foreign_key
45
+
46
+ foreign_key ||= default_foreign_key
47
+ define_method(model) do
48
+ relation_class = Helper.model_class(class_name || model)
49
+ return relation_class.find_by(foreign_key => index) unless as
50
+
51
+ relation_class.find_by("#{as}_id" => id,
52
+ "#{as}_type" => self.class.name)
53
+ end
54
+ end
55
+ # rubocop:enable Naming/PredicateName
56
+ # rubocop:enable Naming/UncommunicativeMethodParamName
57
+
58
+ private
59
+
60
+ def default_foreign_key
61
+ "#{name.to_s.underscore.singularize}_#{index}"
62
+ end
63
+
64
+ def raise_options_error
65
+ raise UnacceptableOptionsError, "Relations with an 'as' and"\
66
+ " 'foreign_key' are not supported."
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Ymodel
6
+ # Represents a schema of all keys found in the source.
7
+ class Schema
8
+ extend Forwardable
9
+ include Enumerable
10
+ attr_reader :attributes
11
+
12
+ def_delegators :attributes, :each
13
+
14
+ def initialize(source, *keys)
15
+ @attributes = source.flat_map(&:keys)
16
+ .map(&:to_sym)
17
+ .to_set + keys
18
+ end
19
+
20
+ def include?(key)
21
+ attributes.include?(key.to_sym)
22
+ end
23
+
24
+ def <<(attribute)
25
+ @attributes << attribute.to_sym
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ymodel
4
+ # Handles events that decorate the instances with methods.
5
+ module Triggerable
6
+ protected
7
+
8
+ def define_readers(model)
9
+ model.instance_eval do
10
+ schema.attributes.each { |attribute| define_reader(attribute) }
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def inherited(model)
17
+ define_readers(model)
18
+ @triggered = true
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ymodel
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/ymodel.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ymodel/version"
4
+ require_relative "ymodel/base"
4
5
 
5
6
  module Ymodel
7
+
6
8
  class Error < StandardError; end
7
9
  # Your code goes here...
8
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ymodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Kemp
@@ -20,10 +20,19 @@ files:
20
20
  - ".rspec"
21
21
  - CHANGELOG.md
22
22
  - Gemfile
23
+ - Gemfile.lock
23
24
  - LICENSE.txt
24
25
  - README.md
25
26
  - Rakefile
26
27
  - lib/ymodel.rb
28
+ - lib/ymodel/base.rb
29
+ - lib/ymodel/errors.rb
30
+ - lib/ymodel/helper.rb
31
+ - lib/ymodel/loadable.rb
32
+ - lib/ymodel/queryable.rb
33
+ - lib/ymodel/relatable.rb
34
+ - lib/ymodel/schema.rb
35
+ - lib/ymodel/triggerable.rb
27
36
  - lib/ymodel/version.rb
28
37
  - sig/ymodel.rbs
29
38
  homepage: