Module: JSON::LD::Frame

Includes:
Utils
Included in:
API
Defined in:
lib/json/ld/frame.rb

Instance Method Summary collapse

Methods included from Utils

#blank_node?, #list?, #subject?, #subject_reference?, #value?

Instance Method Details

#cleanup_preserve(input) ⇒ Array, Hash

Replace @preserve keys with the values, also replace @null with null

Parameters:

  • input (Array, Hash)

Returns:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/json/ld/frame.rb', line 255

def cleanup_preserve(input)
  depth do
    #debug("cleanup preserve") {input.inspect}
    result = case input
    when Array
      # If, after replacement, an array contains only the value null remove the value, leaving an empty array. 
      input.map {|o| cleanup_preserve(o)}.compact
    when Hash
      output = Hash.ordered
      input.each do |key, value|
        if key == '@preserve'
          # replace all key-value pairs where the key is @preserve with the value from the key-pair
          output = cleanup_preserve(value)
        else
          v = cleanup_preserve(value)
          
          # Because we may have added a null value to an array, we need to clean that up, if we possible
          v = v.first if v.is_a?(Array) && v.length == 1 &&
            context.expand_iri(key) != "@graph" && context.container(key).nil?
          output[key] = v
        end
      end
      output
    when '@null'
      # If the value from the key-pair is @null, replace the value with nul
      nil
    else
      input
    end
    #debug(" => ") {result.inspect}
    result
  end
end

#flattenArray{Hash}

Flatten input, used in framing.

This algorithm works by transforming input to statements, and then back to JSON-LD

Returns:



239
240
241
242
243
244
245
246
247
248
# File 'lib/json/ld/frame.rb', line 239

def flatten
  debug("flatten")
  expanded = depth {self.expand(self.value, nil, context)}
  statements = []
  depth {self.statements("", expanded, nil, nil, nil ) {|s| statements << s}}
  debug("flatten") {"statements: #{statements.map(&:to_nquads).join("\n")}"}

  # Transform back to JSON-LD, not flattened
  depth {self.from_statements(statements)}
end

#frame(state, subjects, frame, parent, property) ⇒ Object

Frame input. Input is expected in expanded form, but frame is in compacted form.

Parameters:

  • state (Hash{Symbol => Object})

    Current framing state

  • subjects (Hash{String => Hash})

    Map of flattened subjects

  • frame (Hash{String => Object})
  • parent (Hash{String => Object})

    Parent subject or top-level array

  • property (String)

    Property referencing this frame, or null for array.

Raises:



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/json/ld/frame.rb', line 18

def frame(state, subjects, frame, parent, property)
  raise ProcessingError, "why isn't @subjects a hash?: #{@subjects.inspect}" unless @subjects.is_a?(Hash)
  depth do
    debug("frame") {"state: #{state.inspect}"}
    debug("frame") {"subjects: #{subjects.keys.inspect}"}
    debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
    debug("frame") {"parent: #{parent.to_json(JSON_STATE)}"}
    debug("frame") {"property: #{property.inspect}"}
    # Validate the frame
    validate_frame(state, frame)

    # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
    # This gives us a hash of objects indexed by @id
    matches = filter_subjects(state, subjects, frame)
    debug("frame") {"matches: #{matches.keys.inspect}"}

    # Get values for embedOn and explicitOn
    embed = get_frame_flag(state, frame, 'embed');
    explicit = get_frame_flag(state, frame, 'explicit');
    debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
  
    # For each id and subject from the set of matched subjects ordered by id
    matches.keys.sort.each do |id|
      element = matches[id]
      # If the active property is null, set the map of embeds in state to an empty map
      state = state.merge(:embeds => {}) if property.nil?

      output = {'@id' => id}
    
      # prepare embed meta info
      embedded_subject = {:parent => parent, :property => property}
    
      # If embedOn is true, and id is in map of embeds from state
      if embed && (existing = state[:embeds].fetch(id, nil))
        # only overwrite an existing embed if it has already been added to its
        # parent -- otherwise its parent is somewhere up the tree from this
        # embed and the embed would occur twice once the tree is added
        embed = false
      
        embed = if existing[:parent].is_a?(Array)
          # If existing has a parent which is an array containing a JSON object with @id equal to id, element has already been embedded and can be overwritten, so set embedOn to true
          existing[:parent].detect {|p| p['@id'] == id}
        else
          # Otherwise, existing has a parent which is a subject definition. Set embedOn to true if any of the items in parent property is a subject definition or subject reference for id because the embed can be overwritten
          existing[:parent].fetch(existing[:property], []).any? do |v|
            v.is_a?(Hash) && v.fetch('@id', nil) == id
          end
        end
        debug("frame") {"embed now: #{embed.inspect}"}

        # If embedOn is true, existing is already embedded but can be overwritten
        remove_embed(state, id) if embed
      end

      unless embed
        # not embedding, add output without any other properties
        add_frame_output(state, parent, property, output)
      else
        # Add embed to map of embeds for id
        state[:embeds][id] = embedded_subject
        debug("frame") {"add embedded_subject: #{embedded_subject.inspect}"}
    
        # Process each property and value in the matched subject as follows
        element.keys.sort.each do |prop|
          value = element[prop]
          if prop[0,1] == '@'
            # If property is a keyword, add property and a copy of value to output and continue with the next property from subject
            output[prop] = value.dup
            next
          end

          # If property is not in frame:
          unless frame.has_key?(prop)
            debug("frame") {"non-framed property #{prop}"}
            # If explicitOn is false, Embed values from subject in output using subject as element and property as active property
            embed_values(state, element, prop, output) unless explicit
            
            # Continue to next property
            next
          end
      
          # Process each item from value as follows
          value.each do |item|
            debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
            
            # FIXME: If item is a JSON object with the key @list
            if list?(item)
              # create a JSON object named list with the key @list and the value of an empty array
              list = {'@list' => []}
              
              # Append list to property in output
              add_frame_output(state, output, prop, list)
              
              # Process each listitem in the @list array as follows
              item['@list'].each do |listitem|
                if subject_reference?(listitem)
                  itemid = listitem['@id']
                  debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}

                  # If listitem is a subject reference process listitem recursively using this algorithm passing a new map of subjects that contains the @id of listitem as the key and the subject reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
                  frame(state, {itemid => @subjects[itemid]}, frame[prop].first, list, '@list')
                else
                  # Otherwise, append a copy of listitem to @list in list.
                  debug("frame") {"list item of #{prop} non-subject ref #{listitem.inspect}"}
                  add_frame_output(state, list, '@list', listitem)
                end
              end
            elsif subject_reference?(item)
              # If item is a subject reference process item recursively
              # Recurse into sub-objects
              itemid = item['@id']
              debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
              
              # passing a new map as subjects that contains the @id of item as the key and the subject reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
              frame(state, {itemid => @subjects[itemid]}, frame[prop].first, output, prop)
            else
              # Otherwise, append a copy of item to active property in output.
              debug("frame") {"value property #{prop} non-subject ref #{item.inspect}"}
              add_frame_output(state, output, prop, item)
            end
          end
        end

        # Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
        frame.keys.sort.each do |prop|
          next if prop[0,1] == '@' || output.has_key?(prop)
          property_frame = frame[prop]
          debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}

          # Set property frame to the first item in value or a newly created JSON object if value is empty.
          property_frame = property_frame.first || {}

          # Skip to the next property in frame if property is in output or if property frame contains @omitDefault which is true or if it does not contain @omitDefault but the value of omit default flag true.
          next if output.has_key?(prop) || get_frame_flag(state, property_frame, 'omitDefault')

          # Set the value of property in output to a new JSON object with a property @preserve and a value that is a copy of the value of @default in frame if it exists, or the string @null otherwise
          default = property_frame.fetch('@default', '@null').dup
          default = [default] unless default.is_a?(Array)
          output[prop] = [{"@preserve" => default.compact}]
          debug("=>") {"add default #{output[prop].inspect}"}
        end
      
        # Add output to parent
        add_frame_output(state, parent, property, output)
      end
    end
  end
end

#get_framing_subjects(subjects, input, namer) ⇒ Object

Build hash of subjects used for framing. Also returns flattened representation of input.

Parameters:

  • subjects (Hash{String => Hash})

    destination for mapped subjects and their Object representations

  • input (Array, Hash)

    JSON-LD in expanded form

  • namer (BlankNodeNamer)

Returns:

  • input with subject definitions changed to references



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/json/ld/frame.rb', line 178

def get_framing_subjects(subjects, input, namer)
  depth do
    debug("framing subjects") {"input: #{input.inspect}"}
    case input
    when Array
      input.map {|o| get_framing_subjects(subjects, o, namer)}
    when Hash
      case
      when subject?(input) || subject_reference?(input)
        # Get name for subject, mapping old blank node identifiers to new
        name = blank_node?(input) ? namer.get_name(input.fetch('@id', nil)) : input['@id']
        debug("framing subjects") {"new subject: #{name.inspect}"} unless subjects.has_key?(name)
        subject = subjects[name] ||= {'@id' => name}

        # In property order
        input.keys.sort.each do |prop|
          value = input[prop]
          case prop
          when '@id'
            # Skip @id, already assigned
          when /^@/
            # Copy other keywords
            subject[prop] = value
          else
            case value
            when Hash
              # Special case @list, which is not in expanded form
              raise InvalidFrame::Syntax, "Unexpected hash value: #{value.inspect}" unless value.has_key?('@list')
            
              # Map entries replacing subjects with subject references
              subject[prop] = {"@list" =>
                value['@list'].map {|o| get_framing_subjects(subjects, o, namer)}
              }
            when Array
              # Map array entries
              subject[prop] = get_framing_subjects(subjects, value, namer)
            else
              raise InvalidFrame::Syntax, "unexpected value: #{value.inspect}"
            end
          end
        end
        
        # Return as subject reference
        {"@id" => name}
      else
        # At this point, it's not a subject or a reference, just return input
        input
      end
    else
      # Returns equivalent representation
      input
    end
  end
end

#remove_dependents(id, embeds) ⇒ Object

recursively remove dependent dangling embeds



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/json/ld/frame.rb', line 376

def remove_dependents(id, embeds)
  debug("frame") {"remove dependents for #{id}"}

  depth do
    # get embed keys as a separate array to enable deleting keys in map
    embeds.each do |id_dep, e|
      p = e.fetch(:parent, {}) if e.is_a?(Hash)
      next unless p.is_a?(Hash)
      pid = p.fetch('@id', nil)
      if pid == id
        debug("frame") {"remove #{id_dep} from embeds"}
        embeds.delete(id_dep)
        remove_dependents(id_dep, embeds)
      end
    end
  end
end