ymodel 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: